--- /dev/null
+/config/additional_environment.rb
+/config/database.yml
+/config/email.yml
+/config/initializers/session_store.rb
+/coverage
+/db/*.db
+/db/*.sqlite3
+/db/schema.rb
+/files/*
+/log/*.log*
+/log/mongrel_debug
+/public/dispatch.*
+/public/plugin_assets
+/tmp/*
+/tmp/cache/*
+/tmp/sessions/*
+/tmp/sockets/*
+/tmp/test/*
+/vendor/rails
--- /dev/null
+= Redmine
+
+Redmine is a flexible project management web application written using Ruby on Rails framework.
+
+More details can be found at http://www.redmine.org
--- /dev/null
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
\ No newline at end of file
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class AccountController < ApplicationController
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ # prevents login action to be filtered by check_if_login_required application scope filter
+ skip_before_filter :check_if_login_required
+
+ # Login request and validation
+ def login
+ if request.get?
+ # Logout user
+ self.logged_user = nil
+ else
+ # Authenticate user
+ if Setting.openid? && using_open_id?
+ open_id_authenticate(params[:openid_url])
+ else
+ password_authentication
+ end
+ end
+ end
+
+ # Log out current user and redirect to welcome page
+ def logout
+ cookies.delete :autologin
+ Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
+ self.logged_user = nil
+ redirect_to home_url
+ end
+
+ # Enable user to choose a new password
+ def lost_password
+ redirect_to(home_url) && return unless Setting.lost_password?
+ if params[:token]
+ @token = Token.find_by_action_and_value("recovery", params[:token])
+ redirect_to(home_url) && return unless @token and !@token.expired?
+ @user = @token.user
+ if request.post?
+ @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
+ if @user.save
+ @token.destroy
+ flash[:notice] = l(:notice_account_password_updated)
+ redirect_to :action => 'login'
+ return
+ end
+ end
+ render :template => "account/password_recovery"
+ return
+ else
+ if request.post?
+ user = User.find_by_mail(params[:mail])
+ # user not found in db
+ flash.now[:error] = l(:notice_account_unknown_email) and return unless user
+ # user uses an external authentification
+ flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
+ # create a new token for password recovery
+ token = Token.new(:user => user, :action => "recovery")
+ if token.save
+ Mailer.deliver_lost_password(token)
+ flash[:notice] = l(:notice_account_lost_email_sent)
+ redirect_to :action => 'login'
+ return
+ end
+ end
+ end
+ end
+
+ # User self-registration
+ def register
+ redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
+ if request.get?
+ session[:auth_source_registration] = nil
+ @user = User.new(:language => Setting.default_language)
+ else
+ @user = User.new(params[:user])
+ @user.admin = false
+ @user.status = User::STATUS_REGISTERED
+ if session[:auth_source_registration]
+ @user.status = User::STATUS_ACTIVE
+ @user.login = session[:auth_source_registration][:login]
+ @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
+ if @user.save
+ session[:auth_source_registration] = nil
+ self.logged_user = @user
+ flash[:notice] = l(:notice_account_activated)
+ redirect_to :controller => 'my', :action => 'account'
+ end
+ else
+ @user.login = params[:user][:login]
+ @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
+
+ case Setting.self_registration
+ when '1'
+ register_by_email_activation(@user)
+ when '3'
+ register_automatically(@user)
+ else
+ register_manually_by_administrator(@user)
+ end
+ end
+ end
+ end
+
+ # Token based account activation
+ def activate
+ redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
+ token = Token.find_by_action_and_value('register', params[:token])
+ redirect_to(home_url) && return unless token and !token.expired?
+ user = token.user
+ redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
+ user.status = User::STATUS_ACTIVE
+ if user.save
+ token.destroy
+ flash[:notice] = l(:notice_account_activated)
+ end
+ redirect_to :action => 'login'
+ end
+
+ private
+
+ def password_authentication
+ user = User.try_to_login(params[:username], params[:password])
+ if user.nil?
+ # Invalid credentials
+ flash.now[:error] = l(:notice_account_invalid_creditentials)
+ elsif user.new_record?
+ # Onthefly creation failed, display the registration form to fill/fix attributes
+ @user = user
+ session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
+ render :action => 'register'
+ else
+ # Valid user
+ successful_authentication(user)
+ end
+ end
+
+
+ def open_id_authenticate(openid_url)
+ authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url) do |result, identity_url, registration|
+ if result.successful?
+ user = User.find_or_initialize_by_identity_url(identity_url)
+ if user.new_record?
+ # Self-registration off
+ redirect_to(home_url) && return unless Setting.self_registration?
+
+ # Create on the fly
+ user.login = registration['nickname'] unless registration['nickname'].nil?
+ user.mail = registration['email'] unless registration['email'].nil?
+ user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
+ user.random_password
+ user.status = User::STATUS_REGISTERED
+
+ case Setting.self_registration
+ when '1'
+ register_by_email_activation(user) do
+ onthefly_creation_failed(user)
+ end
+ when '3'
+ register_automatically(user) do
+ onthefly_creation_failed(user)
+ end
+ else
+ register_manually_by_administrator(user) do
+ onthefly_creation_failed(user)
+ end
+ end
+ else
+ # Existing record
+ if user.active?
+ successful_authentication(user)
+ else
+ account_pending
+ end
+ end
+ end
+ end
+ end
+
+ def successful_authentication(user)
+ # Valid user
+ self.logged_user = user
+ # generate a key and set cookie if autologin
+ if params[:autologin] && Setting.autologin?
+ token = Token.create(:user => user, :action => 'autologin')
+ cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
+ end
+ call_hook(:controller_account_success_authentication_after, {:user => user })
+ redirect_back_or_default :controller => 'my', :action => 'page'
+ end
+
+ # Onthefly creation failed, display the registration form to fill/fix attributes
+ def onthefly_creation_failed(user, auth_source_options = { })
+ @user = user
+ session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
+ render :action => 'register'
+ end
+
+ # Register a user for email activation.
+ #
+ # Pass a block for behavior when a user fails to save
+ def register_by_email_activation(user, &block)
+ token = Token.new(:user => user, :action => "register")
+ if user.save and token.save
+ Mailer.deliver_register(token)
+ flash[:notice] = l(:notice_account_register_done)
+ redirect_to :action => 'login'
+ else
+ yield if block_given?
+ end
+ end
+
+ # Automatically register a user
+ #
+ # Pass a block for behavior when a user fails to save
+ def register_automatically(user, &block)
+ # Automatic activation
+ user.status = User::STATUS_ACTIVE
+ user.last_login_on = Time.now
+ if user.save
+ self.logged_user = user
+ flash[:notice] = l(:notice_account_activated)
+ redirect_to :controller => 'my', :action => 'account'
+ else
+ yield if block_given?
+ end
+ end
+
+ # Manual activation by the administrator
+ #
+ # Pass a block for behavior when a user fails to save
+ def register_manually_by_administrator(user, &block)
+ if user.save
+ # Sends an email to the administrators
+ Mailer.deliver_account_activation_request(user)
+ account_pending
+ else
+ yield if block_given?
+ end
+ end
+
+ def account_pending
+ flash[:notice] = l(:notice_account_pending)
+ redirect_to :action => 'login'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class AdminController < ApplicationController
+ before_filter :require_admin
+
+ helper :sort
+ include SortHelper
+
+ def index
+ @no_configuration_data = Redmine::DefaultData::Loader::no_data?
+ end
+
+ def projects
+ @status = params[:status] ? params[:status].to_i : 1
+ c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
+
+ unless params[:name].blank?
+ name = "%#{params[:name].strip.downcase}%"
+ c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
+ end
+
+ @projects = Project.find :all, :order => 'lft',
+ :conditions => c.conditions
+
+ render :action => "projects", :layout => false if request.xhr?
+ end
+
+ def plugins
+ @plugins = Redmine::Plugin.all
+ end
+
+ # Loads the default configuration
+ # (roles, trackers, statuses, workflow, enumerations)
+ def default_configuration
+ if request.post?
+ begin
+ Redmine::DefaultData::Loader::load(params[:lang])
+ flash[:notice] = l(:notice_default_data_loaded)
+ rescue Exception => e
+ flash[:error] = l(:error_can_t_load_default_data, e.message)
+ end
+ end
+ redirect_to :action => 'index'
+ end
+
+ def test_email
+ raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
+ # Force ActionMailer to raise delivery errors so we can catch it
+ ActionMailer::Base.raise_delivery_errors = true
+ begin
+ @test = Mailer.deliver_test(User.current)
+ flash[:notice] = l(:notice_email_sent, User.current.mail)
+ rescue Exception => e
+ flash[:error] = l(:notice_email_error, e.message)
+ end
+ ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
+ redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
+ end
+
+ def info
+ @db_adapter_name = ActiveRecord::Base.connection.adapter_name
+ @flags = {
+ :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
+ :file_repository_writable => File.writable?(Attachment.storage_path),
+ :plugin_assets_writable => File.writable?(Engines.public_directory),
+ :rmagick_available => Object.const_defined?(:Magick)
+ }
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'uri'
+require 'cgi'
+
+class ApplicationController < ActionController::Base
+ include Redmine::I18n
+
+ layout 'base'
+
+ before_filter :user_setup, :check_if_login_required, :set_localization
+ filter_parameter_logging :password
+ protect_from_forgery
+
+ include Redmine::Search::Controller
+ include Redmine::MenuManager::MenuController
+ helper Redmine::MenuManager::MenuHelper
+
+ REDMINE_SUPPORTED_SCM.each do |scm|
+ require_dependency "repository/#{scm.underscore}"
+ end
+
+ def user_setup
+ # Check the settings cache for each request
+ Setting.check_cache
+ # Find the current user
+ User.current = find_current_user
+ end
+
+ # Returns the current user or nil if no user is logged in
+ # and starts a session if needed
+ def find_current_user
+ if session[:user_id]
+ # existing session
+ (User.active.find(session[:user_id]) rescue nil)
+ elsif cookies[:autologin] && Setting.autologin?
+ # auto-login feature starts a new session
+ user = User.try_to_autologin(cookies[:autologin])
+ session[:user_id] = user.id if user
+ user
+ elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
+ # RSS key authentication does not start a session
+ User.find_by_rss_key(params[:key])
+ end
+ end
+
+ # Sets the logged in user
+ def logged_user=(user)
+ reset_session
+ if user && user.is_a?(User)
+ User.current = user
+ session[:user_id] = user.id
+ else
+ User.current = User.anonymous
+ end
+ end
+
+ # check if login is globally required to access the application
+ def check_if_login_required
+ # no check needed if user is already logged in
+ return true if User.current.logged?
+ require_login if Setting.login_required?
+ end
+
+ def set_localization
+ lang = nil
+ if User.current.logged?
+ lang = find_language(User.current.language)
+ end
+ if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
+ accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
+ if !accept_lang.blank?
+ lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
+ end
+ end
+ lang ||= Setting.default_language
+ set_language_if_valid(lang)
+ end
+
+ def require_login
+ if !User.current.logged?
+ # Extract only the basic url parameters on non-GET requests
+ if request.get?
+ url = url_for(params)
+ else
+ url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
+ end
+ redirect_to :controller => "account", :action => "login", :back_url => url
+ return false
+ end
+ true
+ end
+
+ def require_admin
+ return unless require_login
+ if !User.current.admin?
+ render_403
+ return false
+ end
+ true
+ end
+
+ def deny_access
+ User.current.logged? ? render_403 : require_login
+ end
+
+ # Authorize the user for the requested action
+ def authorize(ctrl = params[:controller], action = params[:action], global = false)
+ allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
+ allowed ? true : deny_access
+ end
+
+ # Authorize the user for the requested action outside a project
+ def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
+ authorize(ctrl, action, global)
+ end
+
+ # make sure that the user is a member of the project (or admin) if project is private
+ # used as a before_filter for actions that do not require any particular permission on the project
+ def check_project_privacy
+ if @project && @project.active?
+ if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
+ true
+ else
+ User.current.logged? ? render_403 : require_login
+ end
+ else
+ @project = nil
+ render_404
+ false
+ end
+ end
+
+ def redirect_back_or_default(default)
+ back_url = CGI.unescape(params[:back_url].to_s)
+ if !back_url.blank?
+ begin
+ uri = URI.parse(back_url)
+ # do not redirect user to another host or to the login or register page
+ if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
+ redirect_to(back_url) and return
+ end
+ rescue URI::InvalidURIError
+ # redirect to default
+ end
+ end
+ redirect_to default
+ end
+
+ def render_403
+ @project = nil
+ render :template => "common/403", :layout => !request.xhr?, :status => 403
+ return false
+ end
+
+ def render_404
+ render :template => "common/404", :layout => !request.xhr?, :status => 404
+ return false
+ end
+
+ def render_error(msg)
+ flash.now[:error] = msg
+ render :text => '', :layout => !request.xhr?, :status => 500
+ end
+
+ def render_feed(items, options={})
+ @items = items || []
+ @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
+ @items = @items.slice(0, Setting.feeds_limit.to_i)
+ @title = options[:title] || Setting.app_title
+ render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
+ end
+
+ def self.accept_key_auth(*actions)
+ actions = actions.flatten.map(&:to_s)
+ write_inheritable_attribute('accept_key_auth_actions', actions)
+ end
+
+ def accept_key_auth_actions
+ self.class.read_inheritable_attribute('accept_key_auth_actions') || []
+ end
+
+ # TODO: move to model
+ def attach_files(obj, attachments)
+ attached = []
+ unsaved = []
+ if attachments && attachments.is_a?(Hash)
+ attachments.each_value do |attachment|
+ file = attachment['file']
+ next unless file && file.size > 0
+ a = Attachment.create(:container => obj,
+ :file => file,
+ :description => attachment['description'].to_s.strip,
+ :author => User.current)
+ a.new_record? ? (unsaved << a) : (attached << a)
+ end
+ if unsaved.any?
+ flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
+ end
+ end
+ attached
+ end
+
+ # Returns the number of objects that should be displayed
+ # on the paginated list
+ def per_page_option
+ per_page = nil
+ if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
+ per_page = params[:per_page].to_s.to_i
+ session[:per_page] = per_page
+ elsif session[:per_page]
+ per_page = session[:per_page]
+ else
+ per_page = Setting.per_page_options_array.first || 25
+ end
+ per_page
+ end
+
+ # qvalues http header parser
+ # code taken from webrick
+ def parse_qvalues(value)
+ tmp = []
+ if value
+ parts = value.split(/,\s*/)
+ parts.each {|part|
+ if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
+ val = m[1]
+ q = (m[2] or 1).to_f
+ tmp.push([val, q])
+ end
+ }
+ tmp = tmp.sort_by{|val, q| -q}
+ tmp.collect!{|val, q| val}
+ end
+ return tmp
+ rescue
+ nil
+ end
+
+ # Returns a string that can be used as filename value in Content-Disposition header
+ def filename_for_content_disposition(name)
+ request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 AttachmentsController < ApplicationController
+ before_filter :find_project
+ before_filter :file_readable, :read_authorize, :except => :destroy
+ before_filter :delete_authorize, :only => :destroy
+
+ verify :method => :post, :only => :destroy
+
+ def show
+ if @attachment.is_diff?
+ @diff = File.new(@attachment.diskfile, "rb").read
+ render :action => 'diff'
+ elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
+ @content = File.new(@attachment.diskfile, "rb").read
+ render :action => 'file'
+ else
+ download
+ end
+ end
+
+ def download
+ if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
+ @attachment.increment_download
+ end
+
+ # images are sent inline
+ send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
+ :type => @attachment.content_type,
+ :disposition => (@attachment.image? ? 'inline' : 'attachment')
+
+ end
+
+ def destroy
+ # Make sure association callbacks are called
+ @attachment.container.attachments.delete(@attachment)
+ redirect_to :back
+ rescue ::ActionController::RedirectBackError
+ redirect_to :controller => 'projects', :action => 'show', :id => @project
+ end
+
+private
+ def find_project
+ @attachment = Attachment.find(params[:id])
+ # Show 404 if the filename in the url is wrong
+ raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
+ @project = @attachment.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ # Checks that the file exists and is readable
+ def file_readable
+ @attachment.readable? ? true : render_404
+ end
+
+ def read_authorize
+ @attachment.visible? ? true : deny_access
+ end
+
+ def delete_authorize
+ @attachment.deletable? ? true : deny_access
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class AuthSourcesController < ApplicationController
+ before_filter :require_admin
+
+ def index
+ list
+ render :action => 'list' unless request.xhr?
+ end
+
+ # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
+ verify :method => :post, :only => [ :destroy, :create, :update ],
+ :redirect_to => { :action => :list }
+
+ def list
+ @auth_source_pages, @auth_sources = paginate :auth_sources, :per_page => 10
+ render :action => "list", :layout => false if request.xhr?
+ end
+
+ def new
+ @auth_source = AuthSourceLdap.new
+ end
+
+ def create
+ @auth_source = AuthSourceLdap.new(params[:auth_source])
+ if @auth_source.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'list'
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @auth_source = AuthSource.find(params[:id])
+ end
+
+ def update
+ @auth_source = AuthSource.find(params[:id])
+ if @auth_source.update_attributes(params[:auth_source])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'list'
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def test_connection
+ @auth_method = AuthSource.find(params[:id])
+ begin
+ @auth_method.test_connection
+ flash[:notice] = l(:notice_successful_connection)
+ rescue => text
+ flash[:error] = "Unable to connect (#{text})"
+ end
+ redirect_to :action => 'list'
+ end
+
+ def destroy
+ @auth_source = AuthSource.find(params[:id])
+ unless @auth_source.users.find(:first)
+ @auth_source.destroy
+ flash[:notice] = l(:notice_successful_delete)
+ end
+ redirect_to :action => 'list'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 BoardsController < ApplicationController
+ default_search_scope :messages
+ before_filter :find_project, :authorize
+
+ helper :messages
+ include MessagesHelper
+ helper :sort
+ include SortHelper
+ helper :watchers
+ include WatchersHelper
+
+ def index
+ @boards = @project.boards
+ # show the board if there is only one
+ if @boards.size == 1
+ @board = @boards.first
+ show
+ end
+ end
+
+ def show
+ respond_to do |format|
+ format.html {
+ sort_init 'updated_on', 'desc'
+ sort_update 'created_on' => "#{Message.table_name}.created_on",
+ 'replies' => "#{Message.table_name}.replies_count",
+ 'updated_on' => "#{Message.table_name}.updated_on"
+
+ @topic_count = @board.topics.count
+ @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
+ @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
+ :include => [:author, {:last_reply => :author}],
+ :limit => @topic_pages.items_per_page,
+ :offset => @topic_pages.current.offset
+ @message = Message.new
+ render :action => 'show', :layout => !request.xhr?
+ }
+ format.atom {
+ @messages = @board.messages.find :all, :order => 'created_on DESC',
+ :include => [:author, :board],
+ :limit => Setting.feeds_limit.to_i
+ render_feed(@messages, :title => "#{@project}: #{@board}")
+ }
+ end
+ end
+
+ verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
+
+ def new
+ @board = Board.new(params[:board])
+ @board.project = @project
+ if request.post? && @board.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
+ end
+ end
+
+ def edit
+ if request.post? && @board.update_attributes(params[:board])
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
+ end
+ end
+
+ def destroy
+ @board.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:project_id])
+ @board = @project.boards.find(params[:id]) if params[:id]
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class CustomFieldsController < ApplicationController
+ before_filter :require_admin
+
+ def index
+ @custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
+ @tab = params[:tab] || 'IssueCustomField'
+ end
+
+ def new
+ @custom_field = begin
+ if params[:type].to_s.match(/.+CustomField$/)
+ params[:type].to_s.constantize.new(params[:custom_field])
+ end
+ rescue
+ end
+ redirect_to(:action => 'index') and return unless @custom_field.is_a?(CustomField)
+
+ if request.post? and @custom_field.save
+ flash[:notice] = l(:notice_successful_create)
+ call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
+ redirect_to :action => 'index', :tab => @custom_field.class.name
+ end
+ @trackers = Tracker.find(:all, :order => 'position')
+ end
+
+ def edit
+ @custom_field = CustomField.find(params[:id])
+ if request.post? and @custom_field.update_attributes(params[:custom_field])
+ flash[:notice] = l(:notice_successful_update)
+ call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
+ redirect_to :action => 'index', :tab => @custom_field.class.name
+ end
+ @trackers = Tracker.find(:all, :order => 'position')
+ end
+
+ def destroy
+ @custom_field = CustomField.find(params[:id]).destroy
+ redirect_to :action => 'index', :tab => @custom_field.class.name
+ rescue
+ flash[:error] = "Unable to delete custom field"
+ redirect_to :action => 'index'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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
+ before_filter :find_project, :only => [:index, :new]
+ before_filter :find_document, :except => [:index, :new]
+ 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.find :all, :include => [:attachments, :category]
+ case @sort_by
+ when 'date'
+ @grouped = documents.group_by {|d| d.created_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.find(:all, :order => "created_on DESC")
+ end
+
+ def new
+ @document = @project.documents.build(params[:document])
+ if request.post? and @document.save
+ attach_files(@document, params[:attachments])
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'index', :project_id => @project
+ end
+ end
+
+ def edit
+ @categories = DocumentCategory.all
+ if request.post? and @document.update_attributes(params[:document])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'show', :id => @document
+ end
+ end
+
+ def destroy
+ @document.destroy
+ redirect_to :controller => 'documents', :action => 'index', :project_id => @project
+ end
+
+ def add_attachment
+ attachments = attach_files(@document, params[:attachments])
+ Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
+ redirect_to :action => 'show', :id => @document
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_document
+ @document = Document.find(params[:id])
+ @project = @document.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class EnumerationsController < ApplicationController
+ before_filter :require_admin
+
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ def index
+ list
+ render :action => 'list'
+ end
+
+ # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
+ verify :method => :post, :only => [ :destroy, :create, :update ],
+ :redirect_to => { :action => :list }
+
+ def list
+ end
+
+ def new
+ begin
+ @enumeration = params[:type].constantize.new
+ rescue NameError
+ @enumeration = Enumeration.new
+ end
+ end
+
+ def create
+ @enumeration = Enumeration.new(params[:enumeration])
+ @enumeration.type = params[:enumeration][:type]
+ if @enumeration.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'list', :type => @enumeration.type
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @enumeration = Enumeration.find(params[:id])
+ end
+
+ def update
+ @enumeration = Enumeration.find(params[:id])
+ @enumeration.type = params[:enumeration][:type] if params[:enumeration][:type]
+ if @enumeration.update_attributes(params[:enumeration])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'list', :type => @enumeration.type
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ @enumeration = Enumeration.find(params[:id])
+ if !@enumeration.in_use?
+ # No associated objects
+ @enumeration.destroy
+ redirect_to :action => 'index'
+ elsif params[:reassign_to_id]
+ if reassign_to = Enumeration.find_by_type_and_id(@enumeration.type, params[:reassign_to_id])
+ @enumeration.destroy(reassign_to)
+ redirect_to :action => 'index'
+ end
+ end
+ @enumerations = Enumeration.find(:all, :conditions => ['type = (?)', @enumeration.type]) - [@enumeration]
+ #rescue
+ # flash[:error] = 'Unable to delete enumeration'
+ # redirect_to :action => 'index'
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class GroupsController < ApplicationController
+ layout 'base'
+ before_filter :require_admin
+
+ helper :custom_fields
+
+ # GET /groups
+ # GET /groups.xml
+ def index
+ @groups = Group.find(:all, :order => 'lastname')
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @groups }
+ end
+ end
+
+ # GET /groups/1
+ # GET /groups/1.xml
+ def show
+ @group = Group.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @group }
+ end
+ end
+
+ # GET /groups/new
+ # GET /groups/new.xml
+ def new
+ @group = Group.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @group }
+ end
+ end
+
+ # GET /groups/1/edit
+ def edit
+ @group = Group.find(params[:id])
+ end
+
+ # POST /groups
+ # POST /groups.xml
+ def create
+ @group = Group.new(params[:group])
+
+ respond_to do |format|
+ if @group.save
+ flash[:notice] = l(:notice_successful_create)
+ format.html { redirect_to(groups_path) }
+ format.xml { render :xml => @group, :status => :created, :location => @group }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /groups/1
+ # PUT /groups/1.xml
+ def update
+ @group = Group.find(params[:id])
+
+ respond_to do |format|
+ if @group.update_attributes(params[:group])
+ flash[:notice] = l(:notice_successful_update)
+ format.html { redirect_to(groups_path) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /groups/1
+ # DELETE /groups/1.xml
+ def destroy
+ @group = Group.find(params[:id])
+ @group.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(groups_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ def add_users
+ @group = Group.find(params[:id])
+ users = User.find_all_by_id(params[:user_ids])
+ @group.users << users if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-users", :partial => 'groups/users'
+ users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
+ }
+ }
+ end
+ end
+
+ def remove_user
+ @group = Group.find(params[:id])
+ @group.users.delete(User.find(params[:user_id])) if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
+ format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
+ end
+ end
+
+ def autocomplete_for_user
+ @group = Group.find(params[:id])
+ @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
+ render :layout => false
+ end
+
+ def edit_membership
+ @group = Group.find(params[:id])
+ @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
+ @membership.attributes = params[:membership]
+ @membership.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
+ page.visual_effect(:highlight, "member-#{@membership.id}")
+ }
+ }
+ end
+ end
+
+ def destroy_membership
+ @group = Group.find(params[:id])
+ Member.find(params[:membership_id]).destroy if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
+ format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueCategoriesController < ApplicationController
+ menu_item :settings
+ before_filter :find_project, :authorize
+
+ verify :method => :post, :only => :destroy
+
+ def edit
+ if request.post? and @category.update_attributes(params[:category])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
+ end
+ end
+
+ def destroy
+ @issue_count = @category.issues.size
+ if @issue_count == 0
+ # No issue assigned to this category
+ @category.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
+ elsif params[:todo]
+ reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
+ @category.destroy(reassign_to)
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
+ end
+ @categories = @project.issue_categories - [@category]
+ end
+
+private
+ def find_project
+ @category = IssueCategory.find(params[:id])
+ @project = @category.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 IssueRelationsController < ApplicationController
+ before_filter :find_project, :authorize
+
+ def new
+ @relation = IssueRelation.new(params[:relation])
+ @relation.issue_from = @issue
+ if params[:relation] && !params[:relation][:issue_to_id].blank?
+ @relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id])
+ end
+ @relation.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
+ format.js do
+ render :update do |page|
+ page.replace_html "relations", :partial => 'issues/relations'
+ if @relation.errors.empty?
+ page << "$('relation_delay').value = ''"
+ page << "$('relation_issue_to_id').value = ''"
+ end
+ end
+ end
+ end
+ end
+
+ def destroy
+ relation = IssueRelation.find(params[:id])
+ if request.post? && @issue.relations.include?(relation)
+ relation.destroy
+ @issue.reload
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
+ format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
+ end
+ end
+
+private
+ def find_project
+ @issue = Issue.find(params[:issue_id])
+ @project = @issue.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueStatusesController < ApplicationController
+ before_filter :require_admin
+
+ verify :method => :post, :only => [ :destroy, :create, :update, :move ],
+ :redirect_to => { :action => :list }
+
+ def index
+ list
+ render :action => 'list' unless request.xhr?
+ end
+
+ def list
+ @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position"
+ render :action => "list", :layout => false if request.xhr?
+ end
+
+ def new
+ @issue_status = IssueStatus.new
+ end
+
+ def create
+ @issue_status = IssueStatus.new(params[:issue_status])
+ if @issue_status.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'list'
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @issue_status = IssueStatus.find(params[:id])
+ end
+
+ def update
+ @issue_status = IssueStatus.find(params[:id])
+ if @issue_status.update_attributes(params[:issue_status])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'list'
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ IssueStatus.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ rescue
+ flash[:error] = "Unable to delete issue status"
+ redirect_to :action => 'list'
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 IssuesController < ApplicationController
+ menu_item :new_issue, :only => :new
+ default_search_scope :issues
+
+ before_filter :find_issue, :only => [:show, :edit, :reply]
+ before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
+ before_filter :find_project, :only => [:new, :update_form, :preview]
+ before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
+ before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
+ accept_key_auth :index, :show, :changes
+
+ helper :journals
+ helper :projects
+ include ProjectsHelper
+ helper :custom_fields
+ include CustomFieldsHelper
+ helper :issue_relations
+ include IssueRelationsHelper
+ helper :watchers
+ include WatchersHelper
+ helper :attachments
+ include AttachmentsHelper
+ helper :queries
+ helper :sort
+ include SortHelper
+ include IssuesHelper
+ helper :timelog
+ include Redmine::Export::PDF
+
+ verify :method => :post,
+ :only => :destroy,
+ :render => { :nothing => true, :status => :method_not_allowed }
+
+ def index
+ retrieve_query
+ sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
+ sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
+
+ if @query.valid?
+ limit = per_page_option
+ respond_to do |format|
+ format.html { }
+ format.atom { limit = Setting.feeds_limit.to_i }
+ format.csv { limit = Setting.issues_export_limit.to_i }
+ format.pdf { limit = Setting.issues_export_limit.to_i }
+ end
+ @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
+ @issue_pages = Paginator.new self, @issue_count, limit, params['page']
+ @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
+ :include => ([:status, :project, :priority] + @query.include_options),
+ :conditions => @query.statement,
+ :limit => limit,
+ :offset => @issue_pages.current.offset
+ respond_to do |format|
+ format.html {
+ if @query.grouped?
+ # Retrieve the issue count by group
+ @issue_count_by_group = begin
+ Issue.count(:group => @query.group_by_statement, :include => [:status, :project], :conditions => @query.statement)
+ # Rails will raise an (unexpected) error if there's only a nil group value
+ rescue ActiveRecord::RecordNotFound
+ {nil => @issue_count}
+ end
+ end
+ render :template => 'issues/index.rhtml', :layout => !request.xhr?
+ }
+ format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
+ format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
+ format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
+ end
+ else
+ # Send html if the query is not valid
+ render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def changes
+ retrieve_query
+ sort_init 'id', 'desc'
+ sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
+
+ if @query.valid?
+ @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
+ :conditions => @query.statement,
+ :limit => 25,
+ :order => "#{Journal.table_name}.created_on DESC"
+ end
+ @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
+ render :layout => false, :content_type => 'application/atom+xml'
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def show
+ @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
+ @journals.each_with_index {|j,i| j.indice = i+1}
+ @journals.reverse! if User.current.wants_comments_in_reverse_order?
+ @changesets = @issue.changesets
+ @changesets.reverse! if User.current.wants_comments_in_reverse_order?
+ @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
+ @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
+ @priorities = IssuePriority.all
+ @time_entry = TimeEntry.new
+ respond_to do |format|
+ format.html { render :template => 'issues/show.rhtml' }
+ format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
+ format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
+ end
+ end
+
+ # Add a new issue
+ # The new issue will be created from an existing one if copy_from parameter is given
+ def new
+ @issue = Issue.new
+ @issue.copy_from(params[:copy_from]) if params[:copy_from]
+ @issue.project = @project
+ # Tracker must be set before custom field values
+ @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
+ if @issue.tracker.nil?
+ render_error l(:error_no_tracker_in_project)
+ return
+ end
+ if params[:issue].is_a?(Hash)
+ @issue.attributes = params[:issue]
+ @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
+ end
+ @issue.author = User.current
+
+ default_status = IssueStatus.default
+ unless default_status
+ render_error l(:error_no_default_issue_status)
+ return
+ end
+ @issue.status = default_status
+ @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
+
+ if request.get? || request.xhr?
+ @issue.start_date ||= Date.today
+ else
+ requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
+ # Check that the user is allowed to apply the requested status
+ @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
+ if @issue.save
+ attach_files(@issue, params[:attachments])
+ flash[:notice] = l(:notice_successful_create)
+ call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
+ redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
+ { :action => 'show', :id => @issue })
+ return
+ end
+ end
+ @priorities = IssuePriority.all
+ render :layout => !request.xhr?
+ end
+
+ # Attributes that can be updated on workflow transition (without :edit permission)
+ # TODO: make it configurable (at least per role)
+ UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
+
+ def edit
+ @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
+ @priorities = IssuePriority.all
+ @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
+ @time_entry = TimeEntry.new
+
+ @notes = params[:notes]
+ journal = @issue.init_journal(User.current, @notes)
+ # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
+ if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
+ attrs = params[:issue].dup
+ attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
+ attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
+ @issue.attributes = attrs
+ end
+
+ if request.post?
+ @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
+ @time_entry.attributes = params[:time_entry]
+ attachments = attach_files(@issue, params[:attachments])
+ attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
+
+ call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
+
+ if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
+ # Log spend time
+ if User.current.allowed_to?(:log_time, @project)
+ @time_entry.save
+ end
+ if !journal.new_record?
+ # Only send notification if something was actually changed
+ flash[:notice] = l(:notice_successful_update)
+ end
+ call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
+ redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
+ end
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ # Remove the previously added attachments if issue was not updated
+ attachments.each(&:destroy)
+ end
+
+ def reply
+ journal = Journal.find(params[:journal_id]) if params[:journal_id]
+ if journal
+ user = journal.user
+ text = journal.notes
+ else
+ user = @issue.author
+ text = @issue.description
+ end
+ content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
+ content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
+ render(:update) { |page|
+ page.<< "$('notes').value = \"#{content}\";"
+ page.show 'update'
+ page << "Form.Element.focus('notes');"
+ page << "Element.scrollTo('update');"
+ page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
+ }
+ end
+
+ # Bulk edit a set of issues
+ def bulk_edit
+ if request.post?
+ status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
+ priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
+ assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
+ category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
+ fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
+ custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
+
+ unsaved_issue_ids = []
+ @issues.each do |issue|
+ journal = issue.init_journal(User.current, params[:notes])
+ issue.priority = priority if priority
+ issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
+ issue.category = category if category || params[:category_id] == 'none'
+ issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
+ issue.start_date = params[:start_date] unless params[:start_date].blank?
+ issue.due_date = params[:due_date] unless params[:due_date].blank?
+ issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
+ issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
+ call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
+ # Don't save any change to the issue if the user is not authorized to apply the requested status
+ unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
+ # Keep unsaved issue ids to display them in flash error
+ unsaved_issue_ids << issue.id
+ end
+ end
+ if unsaved_issue_ids.empty?
+ flash[:notice] = l(:notice_successful_update) unless @issues.empty?
+ else
+ flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
+ :total => @issues.size,
+ :ids => '#' + unsaved_issue_ids.join(', #'))
+ end
+ redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
+ return
+ end
+ # Find potential statuses the user could be allowed to switch issues to
+ @available_statuses = Workflow.find(:all, :include => :new_status,
+ :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
+ @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
+ end
+
+ def move
+ @allowed_projects = []
+ # find projects to which the user is allowed to move the issue
+ if User.current.admin?
+ # admin is allowed to move issues to any active (visible) project
+ @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
+ else
+ User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
+ end
+ @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
+ @target_project ||= @project
+ @trackers = @target_project.trackers
+ if request.post?
+ new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
+ unsaved_issue_ids = []
+ moved_issues = []
+ @issues.each do |issue|
+ issue.init_journal(User.current)
+ if r = issue.move_to(@target_project, new_tracker, params[:copy_options])
+ moved_issues << r
+ else
+ unsaved_issue_ids << issue.id
+ end
+ end
+ if unsaved_issue_ids.empty?
+ flash[:notice] = l(:notice_successful_update) unless @issues.empty?
+ else
+ flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
+ :total => @issues.size,
+ :ids => '#' + unsaved_issue_ids.join(', #'))
+ end
+ if params[:follow]
+ if @issues.size == 1 && moved_issues.size == 1
+ redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
+ else
+ redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
+ end
+ else
+ redirect_to :controller => 'issues', :action => 'index', :project_id => @project
+ end
+ return
+ end
+ render :layout => false if request.xhr?
+ end
+
+ def destroy
+ @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
+ if @hours > 0
+ case params[:todo]
+ when 'destroy'
+ # nothing to do
+ when 'nullify'
+ TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
+ when 'reassign'
+ reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
+ if reassign_to.nil?
+ flash.now[:error] = l(:error_issue_not_found_in_project)
+ return
+ else
+ TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
+ end
+ else
+ # display the destroy form
+ return
+ end
+ end
+ @issues.each(&:destroy)
+ redirect_to :action => 'index', :project_id => @project
+ end
+
+ def gantt
+ @gantt = Redmine::Helpers::Gantt.new(params)
+ retrieve_query
+ if @query.valid?
+ events = []
+ # Issues that have start and due dates
+ events += Issue.find(:all,
+ :order => "start_date, due_date",
+ :include => [:tracker, :status, :assigned_to, :priority, :project],
+ :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
+ )
+ # Issues that don't have a due date but that are assigned to a version with a date
+ events += Issue.find(:all,
+ :order => "start_date, effective_date",
+ :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
+ :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
+ )
+ # Versions
+ events += Version.find(:all, :include => :project,
+ :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
+
+ @gantt.events = events
+ end
+
+ basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
+
+ respond_to do |format|
+ format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
+ format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
+ format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
+ end
+ end
+
+ def calendar
+ if params[:year] and params[:year].to_i > 1900
+ @year = params[:year].to_i
+ if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
+ @month = params[:month].to_i
+ end
+ end
+ @year ||= Date.today.year
+ @month ||= Date.today.month
+
+ @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
+ retrieve_query
+ if @query.valid?
+ events = []
+ events += Issue.find(:all,
+ :include => [:tracker, :status, :assigned_to, :priority, :project],
+ :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
+ )
+ events += Version.find(:all, :include => :project,
+ :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
+
+ @calendar.events = events
+ end
+
+ render :layout => false if request.xhr?
+ end
+
+ def context_menu
+ @issues = Issue.find_all_by_id(params[:ids], :include => :project)
+ if (@issues.size == 1)
+ @issue = @issues.first
+ @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
+ end
+ projects = @issues.collect(&:project).compact.uniq
+ @project = projects.first if projects.size == 1
+
+ @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
+ :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
+ :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
+ :move => (@project && User.current.allowed_to?(:move_issues, @project)),
+ :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
+ :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
+ }
+ if @project
+ @assignables = @project.assignable_users
+ @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
+ end
+
+ @priorities = IssuePriority.all.reverse
+ @statuses = IssueStatus.find(:all, :order => 'position')
+ @back = params[:back_url] || request.env['HTTP_REFERER']
+
+ render :layout => false
+ end
+
+ def update_form
+ @issue = Issue.new(params[:issue])
+ render :action => :new, :layout => false
+ end
+
+ def preview
+ @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
+ @attachements = @issue.attachments if @issue
+ @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
+ render :partial => 'common/preview'
+ end
+
+private
+ def find_issue
+ @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
+ @project = @issue.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ # Filter for bulk operations
+ def find_issues
+ @issues = Issue.find_all_by_id(params[:id] || params[:ids])
+ raise ActiveRecord::RecordNotFound if @issues.empty?
+ projects = @issues.collect(&:project).compact.uniq
+ if projects.size == 1
+ @project = projects.first
+ else
+ # TODO: let users bulk edit/move/destroy issues from different projects
+ render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ @project = Project.find(params[:project_id]) unless params[:project_id].blank?
+ allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
+ allowed ? true : deny_access
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ # Retrieve query from session or build a new query
+ def retrieve_query
+ if !params[:query_id].blank?
+ cond = "project_id IS NULL"
+ cond << " OR project_id = #{@project.id}" if @project
+ @query = Query.find(params[:query_id], :conditions => cond)
+ @query.project = @project
+ session[:query] = {:id => @query.id, :project_id => @query.project_id}
+ sort_clear
+ else
+ if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
+ # Give it a name, required to be valid
+ @query = Query.new(:name => "_")
+ @query.project = @project
+ if params[:fields] and params[:fields].is_a? Array
+ params[:fields].each do |field|
+ @query.add_filter(field, params[:operators][field], params[:values][field])
+ end
+ else
+ @query.available_filters.keys.each do |field|
+ @query.add_short_filter(field, params[field]) if params[field]
+ end
+ end
+ @query.group_by = params[:group_by]
+ session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
+ else
+ @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
+ @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
+ @query.project = @project
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 JournalsController < ApplicationController
+ before_filter :find_journal
+
+ def edit
+ if request.post?
+ @journal.update_attributes(:notes => params[:notes]) if params[:notes]
+ @journal.destroy if @journal.details.empty? && @journal.notes.blank?
+ call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
+ format.js { render :action => 'update' }
+ end
+ end
+ end
+
+private
+ def find_journal
+ @journal = Journal.find(params[:id])
+ render_403 and return false unless @journal.editable_by?(User.current)
+ @project = @journal.journalized.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 MailHandlerController < ActionController::Base
+ before_filter :check_credential
+
+ verify :method => :post,
+ :only => :index,
+ :render => { :nothing => true, :status => 405 }
+
+ # Submits an incoming email to MailHandler
+ def index
+ options = params.dup
+ email = options.delete(:email)
+ if MailHandler.receive(email, options)
+ render :nothing => true, :status => :created
+ else
+ render :nothing => true, :status => :unprocessable_entity
+ end
+ end
+
+ private
+
+ def check_credential
+ User.current = nil
+ unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key
+ render :nothing => true, :status => 403
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MembersController < ApplicationController
+ before_filter :find_member, :except => [:new, :autocomplete_for_member]
+ before_filter :find_project, :only => [:new, :autocomplete_for_member]
+ before_filter :authorize
+
+ def new
+ members = []
+ if params[:member] && request.post?
+ attrs = params[:member].dup
+ if (user_ids = attrs.delete(:user_ids))
+ user_ids.each do |user_id|
+ members << Member.new(attrs.merge(:user_id => user_id))
+ end
+ else
+ members << Member.new(attrs)
+ end
+ @project.members << members
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-members", :partial => 'projects/settings/members'
+ members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
+ }
+ }
+ end
+ end
+
+ def edit
+ if request.post? and @member.update_attributes(params[:member])
+ respond_to do |format|
+ format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-members", :partial => 'projects/settings/members'
+ page.visual_effect(:highlight, "member-#{@member.id}")
+ }
+ }
+ end
+ end
+ end
+
+ def destroy
+ if request.post? && @member.deletable?
+ @member.destroy
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
+ format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
+ end
+ end
+
+ def autocomplete_for_member
+ @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
+ render :layout => false
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_member
+ @member = Member.find(params[:id])
+ @project = @member.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MessagesController < ApplicationController
+ menu_item :boards
+ default_search_scope :messages
+ before_filter :find_board, :only => [:new, :preview]
+ before_filter :find_message, :except => [:new, :preview]
+ before_filter :authorize, :except => [:preview, :edit, :destroy]
+
+ verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
+ verify :xhr => true, :only => :quote
+
+ helper :watchers
+ helper :attachments
+ include AttachmentsHelper
+
+ # Show a topic and its replies
+ def show
+ @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
+ @replies.reverse! if User.current.wants_comments_in_reverse_order?
+ @reply = Message.new(:subject => "RE: #{@message.subject}")
+ render :action => "show", :layout => false if request.xhr?
+ end
+
+ # Create a new topic
+ def new
+ @message = Message.new(params[:message])
+ @message.author = User.current
+ @message.board = @board
+ if params[:message] && User.current.allowed_to?(:edit_messages, @project)
+ @message.locked = params[:message]['locked']
+ @message.sticky = params[:message]['sticky']
+ end
+ if request.post? && @message.save
+ call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
+ attach_files(@message, params[:attachments])
+ redirect_to :action => 'show', :id => @message
+ end
+ end
+
+ # Reply to a topic
+ def reply
+ @reply = Message.new(params[:reply])
+ @reply.author = User.current
+ @reply.board = @board
+ @topic.children << @reply
+ if !@reply.new_record?
+ call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
+ attach_files(@reply, params[:attachments])
+ end
+ redirect_to :action => 'show', :id => @topic
+ end
+
+ # Edit a message
+ def edit
+ render_403 and return false unless @message.editable_by?(User.current)
+ if params[:message]
+ @message.locked = params[:message]['locked']
+ @message.sticky = params[:message]['sticky']
+ end
+ if request.post? && @message.update_attributes(params[:message])
+ attach_files(@message, params[:attachments])
+ flash[:notice] = l(:notice_successful_update)
+ @message.reload
+ redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
+ end
+ end
+
+ # Delete a messages
+ def destroy
+ render_403 and return false unless @message.destroyable_by?(User.current)
+ @message.destroy
+ redirect_to @message.parent.nil? ?
+ { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
+ { :action => 'show', :id => @message.parent }
+ end
+
+ def quote
+ user = @message.author
+ text = @message.content
+ subject = @message.subject.gsub('"', '\"')
+ subject = "RE: #{subject}" unless subject.starts_with?('RE:')
+ content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
+ content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
+ render(:update) { |page|
+ page << "$('reply_subject').value = \"#{subject}\";"
+ page.<< "$('message_content').value = \"#{content}\";"
+ page.show 'reply'
+ page << "Form.Element.focus('message_content');"
+ page << "Element.scrollTo('reply');"
+ page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
+ }
+ end
+
+ def preview
+ message = @board.messages.find_by_id(params[:id])
+ @attachements = message.attachments if message
+ @text = (params[:message] || params[:reply])[:content]
+ render :partial => 'common/preview'
+ end
+
+private
+ def find_message
+ find_board
+ @message = @board.messages.find(params[:id], :include => :parent)
+ @topic = @message.root
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_board
+ @board = Board.find(params[:board_id], :include => :project)
+ @project = @board.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MyController < ApplicationController
+ before_filter :require_login
+
+ helper :issues
+ helper :custom_fields
+
+ BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
+ 'issuesreportedbyme' => :label_reported_issues,
+ 'issueswatched' => :label_watched_issues,
+ 'news' => :label_news_latest,
+ 'calendar' => :label_calendar,
+ 'documents' => :label_document_plural,
+ 'timelog' => :label_spent_time
+ }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
+
+ DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
+ 'right' => ['issuesreportedbyme']
+ }.freeze
+
+ verify :xhr => true,
+ :session => :page_layout,
+ :only => [:add_block, :remove_block, :order_blocks]
+
+ def index
+ page
+ render :action => 'page'
+ end
+
+ # Show user's page
+ def page
+ @user = User.current
+ @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
+ end
+
+ # Edit user's account
+ def account
+ @user = User.current
+ @pref = @user.pref
+ if request.post?
+ @user.attributes = params[:user]
+ @user.mail_notification = (params[:notification_option] == 'all')
+ @user.pref.attributes = params[:pref]
+ @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
+ if @user.save
+ @user.pref.save
+ @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
+ set_language_if_valid @user.language
+ flash[:notice] = l(:notice_account_updated)
+ redirect_to :action => 'account'
+ return
+ end
+ end
+ @notification_options = [[l(:label_user_mail_option_all), 'all'],
+ [l(:label_user_mail_option_none), 'none']]
+ # Only users that belong to more than 1 project can select projects for which they are notified
+ # Note that @user.membership.size would fail since AR ignores :include association option when doing a count
+ @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
+ @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
+ end
+
+ # Manage user's password
+ def password
+ @user = User.current
+ flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
+ if request.post?
+ if @user.check_password?(params[:password])
+ @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
+ if @user.save
+ flash[:notice] = l(:notice_account_password_updated)
+ redirect_to :action => 'account'
+ end
+ else
+ flash[:error] = l(:notice_account_wrong_password)
+ end
+ end
+ end
+
+ # Create a new feeds key
+ def reset_rss_key
+ if request.post? && User.current.rss_token
+ User.current.rss_token.destroy
+ flash[:notice] = l(:notice_feeds_access_key_reseted)
+ end
+ redirect_to :action => 'account'
+ end
+
+ # User's page layout configuration
+ def page_layout
+ @user = User.current
+ @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
+ session[:page_layout] = @blocks
+ %w(top left right).each {|f| session[:page_layout][f] ||= [] }
+ @block_options = []
+ BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
+ end
+
+ # Add a block to user's page
+ # The block is added on top of the page
+ # params[:block] : id of the block to add
+ def add_block
+ block = params[:block].to_s.underscore
+ render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
+ @user = User.current
+ # remove if already present in a group
+ %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
+ # add it on top
+ session[:page_layout]['top'].unshift block
+ render :partial => "block", :locals => {:user => @user, :block_name => block}
+ end
+
+ # Remove a block to user's page
+ # params[:block] : id of the block to remove
+ def remove_block
+ block = params[:block].to_s.underscore
+ # remove block in all groups
+ %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
+ render :nothing => true
+ end
+
+ # Change blocks order on user's page
+ # params[:group] : group to order (top, left or right)
+ # params[:list-(top|left|right)] : array of block ids of the group
+ def order_blocks
+ group = params[:group]
+ if group.is_a?(String)
+ group_items = (params["list-#{group}"] || []).collect(&:underscore)
+ if group_items and group_items.is_a? Array
+ # remove group blocks if they are presents in other groups
+ %w(top left right).each {|f|
+ session[:page_layout][f] = (session[:page_layout][f] || []) - group_items
+ }
+ session[:page_layout][group] = group_items
+ end
+ end
+ render :nothing => true
+ end
+
+ # Save user's page layout
+ def page_layout_save
+ @user = User.current
+ @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
+ @user.pref.save
+ session[:page_layout] = nil
+ redirect_to :action => 'page'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class NewsController < ApplicationController
+ default_search_scope :news
+ before_filter :find_news, :except => [:new, :index, :preview]
+ before_filter :find_project, :only => [:new, :preview]
+ before_filter :authorize, :except => [:index, :preview]
+ before_filter :find_optional_project, :only => :index
+ accept_key_auth :index
+
+ def index
+ @news_pages, @newss = paginate :news,
+ :per_page => 10,
+ :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
+ :include => [:author, :project],
+ :order => "#{News.table_name}.created_on DESC"
+ respond_to do |format|
+ format.html { render :layout => false if request.xhr? }
+ format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
+ end
+ end
+
+ def show
+ @comments = @news.comments
+ @comments.reverse! if User.current.wants_comments_in_reverse_order?
+ end
+
+ def new
+ @news = News.new(:project => @project, :author => User.current)
+ if request.post?
+ @news.attributes = params[:news]
+ if @news.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'news', :action => 'index', :project_id => @project
+ end
+ end
+ end
+
+ def edit
+ if request.post? and @news.update_attributes(params[:news])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'show', :id => @news
+ end
+ end
+
+ def add_comment
+ @comment = Comment.new(params[:comment])
+ @comment.author = User.current
+ if @news.comments << @comment
+ flash[:notice] = l(:label_comment_added)
+ redirect_to :action => 'show', :id => @news
+ else
+ show
+ render :action => 'show'
+ end
+ end
+
+ def destroy_comment
+ @news.comments.find(params[:comment_id]).destroy
+ redirect_to :action => 'show', :id => @news
+ end
+
+ def destroy
+ @news.destroy
+ redirect_to :action => 'index', :project_id => @project
+ end
+
+ def preview
+ @text = (params[:news] ? params[:news][:description] : nil)
+ render :partial => 'common/preview'
+ end
+
+private
+ def find_news
+ @news = News.find(params[:id])
+ @project = @news.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ return true unless params[:project_id]
+ @project = Project.find(params[:project_id])
+ authorize
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class ProjectsController < ApplicationController
+ menu_item :overview
+ menu_item :activity, :only => :activity
+ menu_item :roadmap, :only => :roadmap
+ menu_item :files, :only => [:list_files, :add_file]
+ menu_item :settings, :only => :settings
+ menu_item :issues, :only => [:changelog]
+
+ before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
+ before_filter :find_optional_project, :only => :activity
+ before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
+ before_filter :authorize_global, :only => :add
+ before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
+ accept_key_auth :activity
+
+ after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
+ if controller.request.post?
+ controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
+ end
+ end
+
+ helper :sort
+ include SortHelper
+ helper :custom_fields
+ include CustomFieldsHelper
+ helper :issues
+ helper IssuesHelper
+ helper :queries
+ include QueriesHelper
+ helper :repositories
+ include RepositoriesHelper
+ include ProjectsHelper
+
+ # Lists visible projects
+ def index
+ respond_to do |format|
+ format.html {
+ @projects = Project.visible.find(:all, :order => 'lft')
+ }
+ format.atom {
+ projects = Project.visible.find(:all, :order => 'created_on DESC',
+ :limit => Setting.feeds_limit.to_i)
+ render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
+ }
+ end
+ end
+
+ # Add a new project
+ def add
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+ @trackers = Tracker.all
+ @project = Project.new(params[:project])
+ if request.get?
+ @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
+ @project.trackers = Tracker.all
+ @project.is_public = Setting.default_projects_public?
+ @project.enabled_module_names = Setting.default_projects_modules
+ else
+ @project.enabled_module_names = params[:enabled_modules]
+ if @project.save
+ @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
+ # Add current user as a project member if he is not admin
+ unless User.current.admin?
+ r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
+ m = Member.new(:user => User.current, :roles => [r])
+ @project.members << m
+ end
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+ end
+ end
+
+ def copy
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+ @trackers = Tracker.all
+ @root_projects = Project.find(:all,
+ :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
+ :order => 'name')
+ @source_project = Project.find(params[:id])
+ if request.get?
+ @project = Project.copy_from(@source_project)
+ if @project
+ @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
+ else
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+ else
+ @project = Project.new(params[:project])
+ @project.enabled_module_names = params[:enabled_modules]
+ if @project.copy(@source_project, :only => params[:only])
+ @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+ end
+ rescue ActiveRecord::RecordNotFound
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+
+ # Show @project
+ def show
+ if params[:jump]
+ # try to redirect to the requested menu item
+ redirect_to_project_menu_item(@project, params[:jump]) && return
+ end
+
+ @users_by_role = @project.users_by_role
+ @subprojects = @project.children.visible
+ @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
+ @trackers = @project.rolled_up_trackers
+
+ cond = @project.project_condition(Setting.display_subprojects_issues?)
+
+ @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
+ :include => [:project, :status, :tracker],
+ :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
+ @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
+ :include => [:project, :status, :tracker],
+ :conditions => cond)
+
+ TimeEntry.visible_by(User.current) do
+ @total_hours = TimeEntry.sum(:hours,
+ :include => :project,
+ :conditions => cond).to_f
+ end
+ @key = User.current.rss_key
+ end
+
+ def settings
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+ @issue_category ||= IssueCategory.new
+ @member ||= @project.members.new
+ @trackers = Tracker.all
+ @repository ||= @project.repository
+ @wiki ||= @project.wiki
+ end
+
+ # Edit @project
+ def edit
+ if request.post?
+ @project.attributes = params[:project]
+ if @project.save
+ @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'settings', :id => @project
+ else
+ settings
+ render :action => 'settings'
+ end
+ end
+ end
+
+ def modules
+ @project.enabled_module_names = params[:enabled_modules]
+ redirect_to :action => 'settings', :id => @project, :tab => 'modules'
+ end
+
+ def archive
+ @project.archive if request.post? && @project.active?
+ redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
+ end
+
+ def unarchive
+ @project.unarchive if request.post? && !@project.active?
+ redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
+ end
+
+ # Delete @project
+ def destroy
+ @project_to_destroy = @project
+ if request.post? and params[:confirm]
+ @project_to_destroy.destroy
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+ # hide project in layout
+ @project = nil
+ end
+
+ # Add a new issue category to @project
+ def add_issue_category
+ @category = @project.issue_categories.build(params[:category])
+ if request.post? and @category.save
+ respond_to do |format|
+ format.html do
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'settings', :tab => 'categories', :id => @project
+ end
+ format.js do
+ # IE doesn't support the replace_html rjs method for select box options
+ render(:update) {|page| page.replace "issue_category_id",
+ content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
+ }
+ end
+ end
+ end
+ end
+
+ # Add a new version to @project
+ def add_version
+ @version = @project.versions.build(params[:version])
+ if request.post? and @version.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'settings', :tab => 'versions', :id => @project
+ end
+ end
+
+ def add_file
+ if request.post?
+ container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
+ attachments = attach_files(container, params[:attachments])
+ if !attachments.empty? && Setting.notified_events.include?('file_added')
+ Mailer.deliver_attachments_added(attachments)
+ end
+ redirect_to :controller => 'projects', :action => 'list_files', :id => @project
+ return
+ end
+ @versions = @project.versions.sort
+ end
+
+ def save_activities
+ if request.post? && params[:enumerations]
+ Project.transaction do
+ params[:enumerations].each do |id, activity|
+ @project.update_or_create_time_entry_activity(id, activity)
+ end
+ end
+ end
+
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
+ end
+
+ def reset_activities
+ @project.time_entry_activities.each do |time_entry_activity|
+ time_entry_activity.destroy(time_entry_activity.parent)
+ end
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
+ end
+
+ def list_files
+ sort_init 'filename', 'asc'
+ sort_update 'filename' => "#{Attachment.table_name}.filename",
+ 'created_on' => "#{Attachment.table_name}.created_on",
+ 'size' => "#{Attachment.table_name}.filesize",
+ 'downloads' => "#{Attachment.table_name}.downloads"
+
+ @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
+ @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
+ render :layout => !request.xhr?
+ end
+
+ # Show changelog for @project
+ def changelog
+ @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
+ retrieve_selected_tracker_ids(@trackers)
+ @versions = @project.versions.sort
+ end
+
+ def roadmap
+ @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
+ retrieve_selected_tracker_ids(@trackers)
+ @versions = @project.versions.sort
+ @versions = @versions.select {|v| !v.completed? } unless params[:completed]
+ end
+
+ def activity
+ @days = Setting.activity_days_default.to_i
+
+ if params[:from]
+ begin; @date_to = params[:from].to_date + 1; rescue; end
+ end
+
+ @date_to ||= Date.today + 1
+ @date_from = @date_to - @days
+ @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
+ @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
+
+ @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
+ :with_subprojects => @with_subprojects,
+ :author => @author)
+ @activity.scope_select {|t| !params["show_#{t}"].nil?}
+ @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
+
+ events = @activity.events(@date_from, @date_to)
+
+ if events.empty? || stale?(:etag => [events.first, User.current])
+ respond_to do |format|
+ format.html {
+ @events_by_day = events.group_by(&:event_date)
+ render :layout => false if request.xhr?
+ }
+ format.atom {
+ title = l(:label_activity)
+ if @author
+ title = @author.name
+ elsif @activity.scope.size == 1
+ title = l("label_#{@activity.scope.first.singularize}_plural")
+ end
+ render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
+ }
+ end
+ end
+
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+private
+ # Find project of id params[:id]
+ # if not found, redirect to project list
+ # Used as a before_filter
+ def find_project
+ @project = Project.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ return true unless params[:id]
+ @project = Project.find(params[:id])
+ authorize
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def retrieve_selected_tracker_ids(selectable_trackers)
+ if ids = params[:tracker_ids]
+ @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
+ else
+ @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class QueriesController < ApplicationController
+ menu_item :issues
+ before_filter :find_query, :except => :new
+ before_filter :find_optional_project, :only => :new
+
+ def new
+ @query = Query.new(params[:query])
+ @query.project = params[:query_is_for_all] ? nil : @project
+ @query.user = User.current
+ @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
+ @query.column_names = nil if params[:default_columns]
+
+ params[:fields].each do |field|
+ @query.add_filter(field, params[:operators][field], params[:values][field])
+ end if params[:fields]
+ @query.group_by ||= params[:group_by]
+
+ if request.post? && params[:confirm] && @query.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
+ return
+ end
+ render :layout => false if request.xhr?
+ end
+
+ def edit
+ if request.post?
+ @query.filters = {}
+ params[:fields].each do |field|
+ @query.add_filter(field, params[:operators][field], params[:values][field])
+ end if params[:fields]
+ @query.attributes = params[:query]
+ @query.project = nil if params[:query_is_for_all]
+ @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
+ @query.column_names = nil if params[:default_columns]
+
+ if @query.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
+ end
+ end
+ end
+
+ def destroy
+ @query.destroy if request.post?
+ redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
+ end
+
+private
+ def find_query
+ @query = Query.find(params[:id])
+ @project = @query.project
+ render_403 unless @query.editable_by?(User.current)
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ @project = Project.find(params[:project_id]) if params[:project_id]
+ User.current.allowed_to?(:save_queries, @project, :global => true)
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class ReportsController < ApplicationController
+ menu_item :issues
+ before_filter :find_project, :authorize
+
+ def issue_report
+ @statuses = IssueStatus.find(:all, :order => 'position')
+
+ case params[:detail]
+ when "tracker"
+ @field = "tracker_id"
+ @rows = @project.trackers
+ @data = issues_by_tracker
+ @report_title = l(:field_tracker)
+ render :template => "reports/issue_report_details"
+ when "version"
+ @field = "fixed_version_id"
+ @rows = @project.versions.sort
+ @data = issues_by_version
+ @report_title = l(:field_version)
+ render :template => "reports/issue_report_details"
+ when "priority"
+ @field = "priority_id"
+ @rows = IssuePriority.all
+ @data = issues_by_priority
+ @report_title = l(:field_priority)
+ render :template => "reports/issue_report_details"
+ when "category"
+ @field = "category_id"
+ @rows = @project.issue_categories
+ @data = issues_by_category
+ @report_title = l(:field_category)
+ render :template => "reports/issue_report_details"
+ when "assigned_to"
+ @field = "assigned_to_id"
+ @rows = @project.members.collect { |m| m.user }
+ @data = issues_by_assigned_to
+ @report_title = l(:field_assigned_to)
+ render :template => "reports/issue_report_details"
+ when "author"
+ @field = "author_id"
+ @rows = @project.members.collect { |m| m.user }
+ @data = issues_by_author
+ @report_title = l(:field_author)
+ render :template => "reports/issue_report_details"
+ when "subproject"
+ @field = "project_id"
+ @rows = @project.descendants.active
+ @data = issues_by_subproject
+ @report_title = l(:field_subproject)
+ render :template => "reports/issue_report_details"
+ else
+ @trackers = @project.trackers
+ @versions = @project.versions.sort
+ @priorities = IssuePriority.all
+ @categories = @project.issue_categories
+ @assignees = @project.members.collect { |m| m.user }
+ @authors = @project.members.collect { |m| m.user }
+ @subprojects = @project.descendants.active
+ issues_by_tracker
+ issues_by_version
+ issues_by_priority
+ issues_by_category
+ issues_by_assigned_to
+ issues_by_author
+ issues_by_subproject
+
+ render :template => "reports/issue_report"
+ end
+ end
+
+private
+ # Find project of id params[:id]
+ def find_project
+ @project = Project.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def issues_by_tracker
+ @issues_by_tracker ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ t.id as tracker_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
+ where
+ i.status_id=s.id
+ and i.tracker_id=t.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, t.id")
+ end
+
+ def issues_by_version
+ @issues_by_version ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ v.id as fixed_version_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
+ where
+ i.status_id=s.id
+ and i.fixed_version_id=v.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, v.id")
+ end
+
+ def issues_by_priority
+ @issues_by_priority ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ p.id as priority_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssuePriority.table_name} p
+ where
+ i.status_id=s.id
+ and i.priority_id=p.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, p.id")
+ end
+
+ def issues_by_category
+ @issues_by_category ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ c.id as category_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
+ where
+ i.status_id=s.id
+ and i.category_id=c.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, c.id")
+ end
+
+ def issues_by_assigned_to
+ @issues_by_assigned_to ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ a.id as assigned_to_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
+ where
+ i.status_id=s.id
+ and i.assigned_to_id=a.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, a.id")
+ end
+
+ def issues_by_author
+ @issues_by_author ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ a.id as author_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
+ where
+ i.status_id=s.id
+ and i.author_id=a.id
+ and i.project_id=#{@project.id}
+ group by s.id, s.is_closed, a.id")
+ end
+
+ def issues_by_subproject
+ @issues_by_subproject ||=
+ ActiveRecord::Base.connection.select_all("select s.id as status_id,
+ s.is_closed as closed,
+ i.project_id as project_id,
+ count(i.id) as total
+ from
+ #{Issue.table_name} i, #{IssueStatus.table_name} s
+ where
+ i.status_id=s.id
+ and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
+ group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
+ @issues_by_subproject ||= []
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'SVG/Graph/Bar'
+require 'SVG/Graph/BarHorizontal'
+require 'digest/sha1'
+
+class ChangesetNotFound < Exception; end
+class InvalidRevisionParam < Exception; end
+
+class RepositoriesController < ApplicationController
+ menu_item :repository
+ default_search_scope :changesets
+
+ before_filter :find_repository, :except => :edit
+ before_filter :find_project, :only => :edit
+ before_filter :authorize
+ accept_key_auth :revisions
+
+ rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
+
+ def edit
+ @repository = @project.repository
+ if !@repository
+ @repository = Repository.factory(params[:repository_scm])
+ @repository.project = @project if @repository
+ end
+ if request.post? && @repository
+ @repository.attributes = params[:repository]
+ @repository.save
+ end
+ render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
+ end
+
+ def committers
+ @committers = @repository.committers
+ @users = @project.users
+ additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
+ @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
+ @users.compact!
+ @users.sort!
+ if request.post? && params[:committers].is_a?(Hash)
+ # Build a hash with repository usernames as keys and corresponding user ids as values
+ @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'committers', :id => @project
+ end
+ end
+
+ def destroy
+ @repository.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
+ end
+
+ def show
+ @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
+
+ @entries = @repository.entries(@path, @rev)
+ if request.xhr?
+ @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
+ else
+ show_error_not_found and return unless @entries
+ @changesets = @repository.latest_changesets(@path, @rev)
+ @properties = @repository.properties(@path, @rev)
+ render :action => 'show'
+ end
+ end
+
+ alias_method :browse, :show
+
+ def changes
+ @entry = @repository.entry(@path, @rev)
+ show_error_not_found and return unless @entry
+ @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
+ @properties = @repository.properties(@path, @rev)
+ end
+
+ def revisions
+ @changeset_count = @repository.changesets.count
+ @changeset_pages = Paginator.new self, @changeset_count,
+ per_page_option,
+ params['page']
+ @changesets = @repository.changesets.find(:all,
+ :limit => @changeset_pages.items_per_page,
+ :offset => @changeset_pages.current.offset,
+ :include => [:user, :repository])
+
+ respond_to do |format|
+ format.html { render :layout => false if request.xhr? }
+ format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
+ end
+ end
+
+ def entry
+ @entry = @repository.entry(@path, @rev)
+ show_error_not_found and return unless @entry
+
+ # If the entry is a dir, show the browser
+ show and return if @entry.is_dir?
+
+ @content = @repository.cat(@path, @rev)
+ show_error_not_found and return unless @content
+ if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
+ # Force the download
+ send_data @content, :filename => @path.split('/').last
+ else
+ # Prevent empty lines when displaying a file with Windows style eol
+ @content.gsub!("\r\n", "\n")
+ end
+ end
+
+ def annotate
+ @entry = @repository.entry(@path, @rev)
+ show_error_not_found and return unless @entry
+
+ @annotate = @repository.scm.annotate(@path, @rev)
+ render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
+ end
+
+ def revision
+ @changeset = @repository.find_changeset_by_name(@rev)
+ raise ChangesetNotFound unless @changeset
+
+ respond_to do |format|
+ format.html
+ format.js {render :layout => false}
+ end
+ rescue ChangesetNotFound
+ show_error_not_found
+ end
+
+ def diff
+ if params[:format] == 'diff'
+ @diff = @repository.diff(@path, @rev, @rev_to)
+ show_error_not_found and return unless @diff
+ filename = "changeset_r#{@rev}"
+ filename << "_r#{@rev_to}" if @rev_to
+ send_data @diff.join, :filename => "#{filename}.diff",
+ :type => 'text/x-patch',
+ :disposition => 'attachment'
+ else
+ @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
+ @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
+
+ # Save diff type as user preference
+ if User.current.logged? && @diff_type != User.current.pref[:diff_type]
+ User.current.pref[:diff_type] = @diff_type
+ User.current.preference.save
+ end
+
+ @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
+ unless read_fragment(@cache_key)
+ @diff = @repository.diff(@path, @rev, @rev_to)
+ show_error_not_found unless @diff
+ end
+ end
+ end
+
+ def stats
+ end
+
+ def graph
+ data = nil
+ case params[:graph]
+ when "commits_per_month"
+ data = graph_commits_per_month(@repository)
+ when "commits_per_author"
+ data = graph_commits_per_author(@repository)
+ end
+ if data
+ headers["Content-Type"] = "image/svg+xml"
+ send_data(data, :type => "image/svg+xml", :disposition => "inline")
+ else
+ render_404
+ end
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_repository
+ @project = Project.find(params[:id])
+ @repository = @project.repository
+ render_404 and return false unless @repository
+ @path = params[:path].join('/') unless params[:path].nil?
+ @path ||= ''
+ @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
+ @rev_to = params[:rev_to]
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ rescue InvalidRevisionParam
+ show_error_not_found
+ end
+
+ def show_error_not_found
+ render_error l(:error_scm_not_found)
+ end
+
+ # Handler for Redmine::Scm::Adapters::CommandFailed exception
+ def show_error_command_failed(exception)
+ render_error l(:error_scm_command_failed, exception.message)
+ end
+
+ def graph_commits_per_month(repository)
+ @date_to = Date.today
+ @date_from = @date_to << 11
+ @date_from = Date.civil(@date_from.year, @date_from.month, 1)
+ commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
+ commits_by_month = [0] * 12
+ commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
+
+ changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
+ changes_by_month = [0] * 12
+ changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
+
+ fields = []
+ 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
+
+ graph = SVG::Graph::Bar.new(
+ :height => 300,
+ :width => 800,
+ :fields => fields.reverse,
+ :stack => :side,
+ :scale_integers => true,
+ :step_x_labels => 2,
+ :show_data_values => false,
+ :graph_title => l(:label_commits_per_month),
+ :show_graph_title => true
+ )
+
+ graph.add_data(
+ :data => commits_by_month[0..11].reverse,
+ :title => l(:label_revision_plural)
+ )
+
+ graph.add_data(
+ :data => changes_by_month[0..11].reverse,
+ :title => l(:label_change_plural)
+ )
+
+ graph.burn
+ end
+
+ def graph_commits_per_author(repository)
+ commits_by_author = repository.changesets.count(:all, :group => :committer)
+ commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
+
+ changes_by_author = repository.changes.count(:all, :group => :committer)
+ h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
+
+ fields = commits_by_author.collect {|r| r.first}
+ commits_data = commits_by_author.collect {|r| r.last}
+ changes_data = commits_by_author.collect {|r| h[r.first] || 0}
+
+ fields = fields + [""]*(10 - fields.length) if fields.length<10
+ commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
+ changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
+
+ # Remove email adress in usernames
+ fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
+
+ graph = SVG::Graph::BarHorizontal.new(
+ :height => 400,
+ :width => 800,
+ :fields => fields,
+ :stack => :side,
+ :scale_integers => true,
+ :show_data_values => false,
+ :rotate_y_labels => false,
+ :graph_title => l(:label_commits_per_author),
+ :show_graph_title => true
+ )
+
+ graph.add_data(
+ :data => commits_data,
+ :title => l(:label_revision_plural)
+ )
+
+ graph.add_data(
+ :data => changes_data,
+ :title => l(:label_change_plural)
+ )
+
+ graph.burn
+ end
+
+end
+
+class Date
+ def months_ago(date = Date.today)
+ (date.year - self.year)*12 + (date.month - self.month)
+ end
+
+ def weeks_ago(date = Date.today)
+ (date.year - self.year)*52 + (date.cweek - self.cweek)
+ end
+end
+
+class String
+ def with_leading_slash
+ starts_with?('/') ? self : "/#{self}"
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class RolesController < ApplicationController
+ before_filter :require_admin
+
+ verify :method => :post, :only => [ :destroy, :move ],
+ :redirect_to => { :action => :list }
+
+ def index
+ list
+ render :action => 'list' unless request.xhr?
+ end
+
+ def list
+ @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
+ render :action => "list", :layout => false if request.xhr?
+ end
+
+ def new
+ # Prefills the form with 'Non member' role permissions
+ @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
+ if request.post? && @role.save
+ # workflow copy
+ if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
+ @role.workflows.copy(copy_from)
+ end
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'index'
+ end
+ @permissions = @role.setable_permissions
+ @roles = Role.find :all, :order => 'builtin, position'
+ end
+
+ def edit
+ @role = Role.find(params[:id])
+ if request.post? and @role.update_attributes(params[:role])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'index'
+ end
+ @permissions = @role.setable_permissions
+ end
+
+ def destroy
+ @role = Role.find(params[:id])
+ @role.destroy
+ redirect_to :action => 'index'
+ rescue
+ flash[:error] = 'This role is in use and can not be deleted.'
+ redirect_to :action => 'index'
+ end
+
+ def report
+ @roles = Role.find(:all, :order => 'builtin, position')
+ @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
+ if request.post?
+ @roles.each do |role|
+ role.permissions = params[:permissions][role.id.to_s]
+ role.save
+ end
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'index'
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class SearchController < ApplicationController
+ before_filter :find_optional_project
+
+ helper :messages
+ include MessagesHelper
+
+ def index
+ @question = params[:q] || ""
+ @question.strip!
+ @all_words = params[:all_words] || (params[:submit] ? false : true)
+ @titles_only = !params[:titles_only].nil?
+
+ projects_to_search =
+ case params[:scope]
+ when 'all'
+ nil
+ when 'my_projects'
+ User.current.memberships.collect(&:project)
+ when 'subprojects'
+ @project ? (@project.self_and_descendants.active) : nil
+ else
+ @project
+ end
+
+ offset = nil
+ begin; offset = params[:offset].to_time if params[:offset]; rescue; end
+
+ # quick jump to an issue
+ if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
+ redirect_to :controller => "issues", :action => "show", :id => $1
+ return
+ end
+
+ @object_types = %w(issues news documents changesets wiki_pages messages projects)
+ if projects_to_search.is_a? Project
+ # don't search projects
+ @object_types.delete('projects')
+ # only show what the user is allowed to view
+ @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
+ end
+
+ @scope = @object_types.select {|t| params[t]}
+ @scope = @object_types if @scope.empty?
+
+ # extract tokens from the question
+ # eg. hello "bye bye" => ["hello", "bye bye"]
+ @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
+ # tokens must be at least 3 character long
+ @tokens = @tokens.uniq.select {|w| w.length > 2 }
+
+ if !@tokens.empty?
+ # no more than 5 tokens to search for
+ @tokens.slice! 5..-1 if @tokens.size > 5
+ # strings used in sql like statement
+ like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
+
+ @results = []
+ @results_by_type = Hash.new {|h,k| h[k] = 0}
+
+ limit = 10
+ @scope.each do |s|
+ r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
+ :all_words => @all_words,
+ :titles_only => @titles_only,
+ :limit => (limit+1),
+ :offset => offset,
+ :before => params[:previous].nil?)
+ @results += r
+ @results_by_type[s] += c
+ end
+ @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
+ if params[:previous].nil?
+ @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
+ if @results.size > limit
+ @pagination_next_date = @results[limit-1].event_datetime
+ @results = @results[0, limit]
+ end
+ else
+ @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
+ if @results.size > limit
+ @pagination_previous_date = @results[-(limit)].event_datetime
+ @results = @results[-(limit), limit]
+ end
+ end
+ else
+ @question = ""
+ end
+ render :layout => false if request.xhr?
+ end
+
+private
+ def find_optional_project
+ return true unless params[:id]
+ @project = Project.find(params[:id])
+ check_project_privacy
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class SettingsController < ApplicationController
+ before_filter :require_admin
+
+ def index
+ edit
+ render :action => 'edit'
+ end
+
+ def edit
+ @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated)
+ if request.post? && params[:settings] && params[:settings].is_a?(Hash)
+ settings = (params[:settings] || {}).dup.symbolize_keys
+ settings.each do |name, value|
+ # remove blank values in array settings
+ value.delete_if {|v| v.blank? } if value.is_a?(Array)
+ Setting[name] = value
+ end
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'edit', :tab => params[:tab]
+ return
+ end
+ @options = {}
+ @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
+ @deliveries = ActionMailer::Base.perform_deliveries
+
+ @guessed_host_and_path = request.host_with_port.dup
+ @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
+ end
+
+ def plugin
+ @plugin = Redmine::Plugin.find(params[:id])
+ if request.post?
+ Setting["plugin_#{@plugin.id}"] = params[:settings]
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'plugin', :id => @plugin.id
+ end
+ @partial = @plugin.settings[:partial]
+ @settings = Setting["plugin_#{@plugin.id}"]
+ rescue Redmine::PluginNotFound
+ render_404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class SysController < ActionController::Base
+ before_filter :check_enabled
+
+ def projects
+ p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
+ render :xml => p.to_xml(:include => :repository)
+ end
+
+ def create_project_repository
+ project = Project.find(params[:id])
+ if project.repository
+ render :nothing => true, :status => 409
+ else
+ logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
+ project.repository = Repository.factory(params[:vendor], params[:repository])
+ if project.repository && project.repository.save
+ render :xml => project.repository, :status => 201
+ else
+ render :nothing => true, :status => 422
+ end
+ end
+ end
+
+ protected
+
+ def check_enabled
+ User.current = nil
+ unless Setting.sys_api_enabled?
+ render :nothing => 'Access denied. Repository management WS is disabled.', :status => 403
+ return false
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 TimelogController < ApplicationController
+ menu_item :issues
+ before_filter :find_project, :authorize, :only => [:edit, :destroy]
+ before_filter :find_optional_project, :only => [:report, :details]
+
+ verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
+
+ helper :sort
+ include SortHelper
+ helper :issues
+ include TimelogHelper
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ def report
+ @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
+ :klass => Project,
+ :label => :label_project},
+ 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
+ :klass => Version,
+ :label => :label_version},
+ 'category' => {:sql => "#{Issue.table_name}.category_id",
+ :klass => IssueCategory,
+ :label => :field_category},
+ 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
+ :klass => User,
+ :label => :label_member},
+ 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
+ :klass => Tracker,
+ :label => :label_tracker},
+ 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
+ :klass => TimeEntryActivity,
+ :label => :label_activity},
+ 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
+ :klass => Issue,
+ :label => :label_issue}
+ }
+
+ # Add list and boolean custom fields as available criterias
+ custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
+ custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
+ :format => cf.field_format,
+ :label => cf.name}
+ end if @project
+
+ # Add list and boolean time entry custom fields
+ TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
+ :format => cf.field_format,
+ :label => cf.name}
+ end
+
+ # Add list and boolean time entry activity custom fields
+ TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
+ :format => cf.field_format,
+ :label => cf.name}
+ end
+
+ @criterias = params[:criterias] || []
+ @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
+ @criterias.uniq!
+ @criterias = @criterias[0,3]
+
+ @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
+
+ retrieve_date_range
+
+ unless @criterias.empty?
+ sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
+ sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
+ sql_condition = ''
+
+ if @project.nil?
+ sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
+ elsif @issue.nil?
+ sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
+ else
+ sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
+ end
+
+ sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
+ sql << " FROM #{TimeEntry.table_name}"
+ sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
+ sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
+ sql << " WHERE"
+ sql << " (%s) AND" % sql_condition
+ sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
+ sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
+
+ @hours = ActiveRecord::Base.connection.select_all(sql)
+
+ @hours.each do |row|
+ case @columns
+ when 'year'
+ row['year'] = row['tyear']
+ when 'month'
+ row['month'] = "#{row['tyear']}-#{row['tmonth']}"
+ when 'week'
+ row['week'] = "#{row['tyear']}-#{row['tweek']}"
+ when 'day'
+ row['day'] = "#{row['spent_on']}"
+ end
+ end
+
+ @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
+
+ @periods = []
+ # Date#at_beginning_of_ not supported in Rails 1.2.x
+ date_from = @from.to_time
+ # 100 columns max
+ while date_from <= @to.to_time && @periods.length < 100
+ case @columns
+ when 'year'
+ @periods << "#{date_from.year}"
+ date_from = (date_from + 1.year).at_beginning_of_year
+ when 'month'
+ @periods << "#{date_from.year}-#{date_from.month}"
+ date_from = (date_from + 1.month).at_beginning_of_month
+ when 'week'
+ @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
+ date_from = (date_from + 7.day).at_beginning_of_week
+ when 'day'
+ @periods << "#{date_from.to_date}"
+ date_from = date_from + 1.day
+ end
+ end
+ end
+
+ respond_to do |format|
+ format.html { render :layout => !request.xhr? }
+ format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
+ end
+ end
+
+ def details
+ sort_init 'spent_on', 'desc'
+ sort_update 'spent_on' => 'spent_on',
+ 'user' => 'user_id',
+ 'activity' => 'activity_id',
+ 'project' => "#{Project.table_name}.name",
+ 'issue' => 'issue_id',
+ 'hours' => 'hours'
+
+ cond = ARCondition.new
+ if @project.nil?
+ cond << Project.allowed_to_condition(User.current, :view_time_entries)
+ elsif @issue.nil?
+ cond << @project.project_condition(Setting.display_subprojects_issues?)
+ else
+ cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
+ end
+
+ retrieve_date_range
+ cond << ['spent_on BETWEEN ? AND ?', @from, @to]
+
+ TimeEntry.visible_by(User.current) do
+ respond_to do |format|
+ format.html {
+ # Paginate results
+ @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
+ @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
+ @entries = TimeEntry.find(:all,
+ :include => [:project, :activity, :user, {:issue => :tracker}],
+ :conditions => cond.conditions,
+ :order => sort_clause,
+ :limit => @entry_pages.items_per_page,
+ :offset => @entry_pages.current.offset)
+ @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
+
+ render :layout => !request.xhr?
+ }
+ format.atom {
+ entries = TimeEntry.find(:all,
+ :include => [:project, :activity, :user, {:issue => :tracker}],
+ :conditions => cond.conditions,
+ :order => "#{TimeEntry.table_name}.created_on DESC",
+ :limit => Setting.feeds_limit.to_i)
+ render_feed(entries, :title => l(:label_spent_time))
+ }
+ format.csv {
+ # Export all entries
+ @entries = TimeEntry.find(:all,
+ :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
+ :conditions => cond.conditions,
+ :order => sort_clause)
+ send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
+ }
+ end
+ end
+ end
+
+ def edit
+ render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
+ @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
+ @time_entry.attributes = params[:time_entry]
+
+ call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
+
+ if request.post? and @time_entry.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_back_or_default :action => 'details', :project_id => @time_entry.project
+ return
+ end
+ end
+
+ def destroy
+ render_404 and return unless @time_entry
+ render_403 and return unless @time_entry.editable_by?(User.current)
+ @time_entry.destroy
+ flash[:notice] = l(:notice_successful_delete)
+ redirect_to :back
+ rescue ::ActionController::RedirectBackError
+ redirect_to :action => 'details', :project_id => @time_entry.project
+ end
+
+private
+ def find_project
+ if params[:id]
+ @time_entry = TimeEntry.find(params[:id])
+ @project = @time_entry.project
+ elsif params[:issue_id]
+ @issue = Issue.find(params[:issue_id])
+ @project = @issue.project
+ elsif params[:project_id]
+ @project = Project.find(params[:project_id])
+ else
+ render_404
+ return false
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ if !params[:issue_id].blank?
+ @issue = Issue.find(params[:issue_id])
+ @project = @issue.project
+ elsif !params[:project_id].blank?
+ @project = Project.find(params[:project_id])
+ end
+ deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
+ end
+
+ # Retrieves the date range based on predefined ranges or specific from/to param dates
+ def retrieve_date_range
+ @free_period = false
+ @from, @to = nil, nil
+
+ if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
+ case params[:period].to_s
+ when 'today'
+ @from = @to = Date.today
+ when 'yesterday'
+ @from = @to = Date.today - 1
+ when 'current_week'
+ @from = Date.today - (Date.today.cwday - 1)%7
+ @to = @from + 6
+ when 'last_week'
+ @from = Date.today - 7 - (Date.today.cwday - 1)%7
+ @to = @from + 6
+ when '7_days'
+ @from = Date.today - 7
+ @to = Date.today
+ when 'current_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1)
+ @to = (@from >> 1) - 1
+ when 'last_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
+ @to = (@from >> 1) - 1
+ when '30_days'
+ @from = Date.today - 30
+ @to = Date.today
+ when 'current_year'
+ @from = Date.civil(Date.today.year, 1, 1)
+ @to = Date.civil(Date.today.year, 12, 31)
+ end
+ elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
+ begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
+ begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
+ @free_period = true
+ else
+ # default
+ end
+
+ @from, @to = @to, @from if @from && @to && @from > @to
+ @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
+ @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class TrackersController < ApplicationController
+ before_filter :require_admin
+
+ def index
+ list
+ render :action => 'list' unless request.xhr?
+ end
+
+ verify :method => :post, :only => :destroy, :redirect_to => { :action => :list }
+
+ def list
+ @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
+ render :action => "list", :layout => false if request.xhr?
+ end
+
+ def new
+ @tracker = Tracker.new(params[:tracker])
+ if request.post? and @tracker.save
+ # workflow copy
+ if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
+ @tracker.workflows.copy(copy_from)
+ end
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'list'
+ return
+ end
+ @trackers = Tracker.find :all, :order => 'position'
+ @projects = Project.find(:all)
+ end
+
+ def edit
+ @tracker = Tracker.find(params[:id])
+ if request.post? and @tracker.update_attributes(params[:tracker])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'list'
+ return
+ end
+ @projects = Project.find(:all)
+ end
+
+ def destroy
+ @tracker = Tracker.find(params[:id])
+ unless @tracker.issues.empty?
+ flash[:error] = "This tracker contains issues and can\'t be deleted."
+ else
+ @tracker.destroy
+ end
+ redirect_to :action => 'list'
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class UsersController < ApplicationController
+ before_filter :require_admin, :except => :show
+
+ helper :sort
+ include SortHelper
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ def index
+ sort_init 'login', 'asc'
+ sort_update %w(login firstname lastname mail admin created_on last_login_on)
+
+ @status = params[:status] ? params[:status].to_i : 1
+ c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
+
+ unless params[:name].blank?
+ name = "%#{params[:name].strip.downcase}%"
+ c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
+ end
+
+ @user_count = User.count(:conditions => c.conditions)
+ @user_pages = Paginator.new self, @user_count,
+ per_page_option,
+ params['page']
+ @users = User.find :all,:order => sort_clause,
+ :conditions => c.conditions,
+ :limit => @user_pages.items_per_page,
+ :offset => @user_pages.current.offset
+
+ render :layout => !request.xhr?
+ end
+
+ def show
+ @user = User.active.find(params[:id])
+ @custom_values = @user.custom_values
+
+ # show only public projects and private projects that the logged in user is also a member of
+ @memberships = @user.memberships.select do |membership|
+ membership.project.is_public? || (User.current.member_of?(membership.project))
+ end
+
+ events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
+ @events_by_day = events.group_by(&:event_date)
+
+ if @user != User.current && !User.current.admin? && @memberships.empty? && events.empty?
+ render_404 and return
+ end
+
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def add
+ if request.get?
+ @user = User.new(:language => Setting.default_language)
+ else
+ @user = User.new(params[:user])
+ @user.admin = params[:user][:admin] || false
+ @user.login = params[:user][:login]
+ @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
+ if @user.save
+ Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :controller => 'users', :action => 'edit', :id => @user
+ end
+ end
+ @auth_sources = AuthSource.find(:all)
+ end
+
+ def edit
+ @user = User.find(params[:id])
+ if request.post?
+ @user.admin = params[:user][:admin] if params[:user][:admin]
+ @user.login = params[:user][:login] if params[:user][:login]
+ @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
+ @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
+ @user.attributes = params[:user]
+ # Was the account actived ? (do it before User#save clears the change)
+ was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
+ if @user.save
+ if was_activated
+ Mailer.deliver_account_activated(@user)
+ elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
+ Mailer.deliver_account_information(@user, params[:password])
+ end
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :back
+ end
+ end
+ @auth_sources = AuthSource.find(:all)
+ @membership ||= Member.new
+ rescue ::ActionController::RedirectBackError
+ redirect_to :controller => 'users', :action => 'edit', :id => @user
+ end
+
+ def edit_membership
+ @user = User.find(params[:id])
+ @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user)
+ @membership.attributes = params[:membership]
+ @membership.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-memberships", :partial => 'users/memberships'
+ page.visual_effect(:highlight, "member-#{@membership.id}")
+ }
+ }
+ end
+ end
+
+ def destroy_membership
+ @user = User.find(params[:id])
+ @membership = Member.find(params[:membership_id])
+ if request.post? && @membership.deletable?
+ @membership.destroy
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
+ format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class VersionsController < ApplicationController
+ menu_item :roadmap
+ before_filter :find_version, :except => :close_completed
+ before_filter :find_project, :only => :close_completed
+ before_filter :authorize
+
+ helper :custom_fields
+
+ def show
+ end
+
+ def edit
+ if request.post? and @version.update_attributes(params[:version])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ end
+ end
+
+ def close_completed
+ if request.post?
+ @project.close_completed_versions
+ end
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ end
+
+ def destroy
+ @version.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ rescue
+ flash[:error] = l(:notice_unable_delete_version)
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ end
+
+ def status_by
+ respond_to do |format|
+ format.html { render :action => 'show' }
+ format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
+ end
+ end
+
+private
+ def find_version
+ @version = Version.find(params[:id])
+ @project = @version.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 WatchersController < ApplicationController
+ before_filter :find_project
+ before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
+ before_filter :authorize, :only => [:new, :destroy]
+
+ verify :method => :post,
+ :only => [ :watch, :unwatch ],
+ :render => { :nothing => true, :status => :method_not_allowed }
+
+ def watch
+ set_watcher(User.current, true)
+ end
+
+ def unwatch
+ set_watcher(User.current, false)
+ end
+
+ def new
+ @watcher = Watcher.new(params[:watcher])
+ @watcher.watchable = @watched
+ @watcher.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :back }
+ format.js do
+ render :update do |page|
+ page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
+ end
+ end
+ end
+ rescue ::ActionController::RedirectBackError
+ render :text => 'Watcher added.', :layout => true
+ end
+
+ def destroy
+ @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
+ respond_to do |format|
+ format.html { redirect_to :back }
+ format.js do
+ render :update do |page|
+ page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
+ end
+ end
+ end
+ end
+
+private
+ def find_project
+ klass = Object.const_get(params[:object_type].camelcase)
+ return false unless klass.respond_to?('watched_by')
+ @watched = klass.find(params[:object_id])
+ @project = @watched.project
+ rescue
+ render_404
+ end
+
+ def set_watcher(user, watching)
+ @watched.set_watcher(user, watching)
+ respond_to do |format|
+ format.html { redirect_to :back }
+ format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
+ end
+ rescue ::ActionController::RedirectBackError
+ render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class WelcomeController < ApplicationController
+ caches_action :robots
+
+ def index
+ @news = News.latest User.current
+ @projects = Project.latest User.current
+ end
+
+ def robots
+ @projects = Project.all_public.active
+ render :layout => false, :content_type => 'text/plain'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'diff'
+
+class WikiController < ApplicationController
+ default_search_scope :wiki_pages
+ before_filter :find_wiki, :authorize
+ before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
+
+ verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
+
+ helper :attachments
+ include AttachmentsHelper
+ helper :watchers
+
+ # display a page (in editing mode if it doesn't exist)
+ def index
+ page_title = params[:page]
+ @page = @wiki.find_or_new_page(page_title)
+ if @page.new_record?
+ if User.current.allowed_to?(:edit_wiki_pages, @project)
+ edit
+ render :action => 'edit'
+ else
+ render_404
+ end
+ return
+ end
+ if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
+ # Redirects user to the current version if he's not allowed to view previous versions
+ redirect_to :version => nil
+ return
+ end
+ @content = @page.content_for_version(params[:version])
+ if params[:format] == 'html'
+ export = render_to_string :action => 'export', :layout => false
+ send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
+ return
+ elsif params[:format] == 'txt'
+ send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
+ return
+ end
+ @editable = editable?
+ render :action => 'show'
+ end
+
+ # edit an existing page or a new one
+ def edit
+ @page = @wiki.find_or_new_page(params[:page])
+ return render_403 unless editable?
+ @page.content = WikiContent.new(:page => @page) if @page.new_record?
+
+ @content = @page.content_for_version(params[:version])
+ @content.text = initial_page_content(@page) if @content.text.blank?
+ # don't keep previous comment
+ @content.comments = nil
+ if request.get?
+ # To prevent StaleObjectError exception when reverting to a previous version
+ @content.version = @page.content.version
+ else
+ if !@page.new_record? && @content.text == params[:content][:text]
+ # don't save if text wasn't changed
+ redirect_to :action => 'index', :id => @project, :page => @page.title
+ return
+ end
+ #@content.text = params[:content][:text]
+ #@content.comments = params[:content][:comments]
+ @content.attributes = params[:content]
+ @content.author = User.current
+ # if page is new @page.save will also save content, but not if page isn't a new record
+ if (@page.new_record? ? @page.save : @content.save)
+ call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
+ redirect_to :action => 'index', :id => @project, :page => @page.title
+ end
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash[:error] = l(:notice_locking_conflict)
+ end
+
+ # rename a page
+ def rename
+ return render_403 unless editable?
+ @page.redirect_existing_links = true
+ # used to display the *original* title if some AR validation errors occur
+ @original_title = @page.pretty_title
+ if request.post? && @page.update_attributes(params[:wiki_page])
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'index', :id => @project, :page => @page.title
+ end
+ end
+
+ def protect
+ @page.update_attribute :protected, params[:protected]
+ redirect_to :action => 'index', :id => @project, :page => @page.title
+ end
+
+ # show page history
+ def history
+ @version_count = @page.content.versions.count
+ @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
+ # don't load text
+ @versions = @page.content.versions.find :all,
+ :select => "id, author_id, comments, updated_on, version",
+ :order => 'version DESC',
+ :limit => @version_pages.items_per_page + 1,
+ :offset => @version_pages.current.offset
+
+ render :layout => false if request.xhr?
+ end
+
+ def diff
+ @diff = @page.diff(params[:version], params[:version_from])
+ render_404 unless @diff
+ end
+
+ def annotate
+ @annotate = @page.annotate(params[:version])
+ render_404 unless @annotate
+ end
+
+ # Removes a wiki page and its history
+ # Children can be either set as root pages, removed or reassigned to another parent page
+ def destroy
+ return render_403 unless editable?
+
+ @descendants_count = @page.descendants.size
+ if @descendants_count > 0
+ case params[:todo]
+ when 'nullify'
+ # Nothing to do
+ when 'destroy'
+ # Removes all its descendants
+ @page.descendants.each(&:destroy)
+ when 'reassign'
+ # Reassign children to another parent page
+ reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
+ return unless reassign_to
+ @page.children.each do |child|
+ child.update_attribute(:parent, reassign_to)
+ end
+ else
+ @reassignable_to = @wiki.pages - @page.self_and_descendants
+ return
+ end
+ end
+ @page.destroy
+ redirect_to :action => 'special', :id => @project, :page => 'Page_index'
+ end
+
+ # display special pages
+ def special
+ page_title = params[:page].downcase
+ case page_title
+ # show pages index, sorted by title
+ when 'page_index', 'date_index'
+ # eager load information about last updates, without loading text
+ @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
+ :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
+ :order => 'title'
+ @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
+ @pages_by_parent_id = @pages.group_by(&:parent_id)
+ # export wiki to a single html file
+ when 'export'
+ @pages = @wiki.pages.find :all, :order => 'title'
+ export = render_to_string :action => 'export_multiple', :layout => false
+ send_data(export, :type => 'text/html', :filename => "wiki.html")
+ return
+ else
+ # requested special page doesn't exist, redirect to default page
+ redirect_to :action => 'index', :id => @project, :page => nil and return
+ end
+ render :action => "special_#{page_title}"
+ end
+
+ def preview
+ page = @wiki.find_page(params[:page])
+ # page is nil when previewing a new page
+ return render_403 unless page.nil? || editable?(page)
+ if page
+ @attachements = page.attachments
+ @previewed = page.content
+ end
+ @text = params[:content][:text]
+ render :partial => 'common/preview'
+ end
+
+ def add_attachment
+ return render_403 unless editable?
+ attach_files(@page, params[:attachments])
+ redirect_to :action => 'index', :page => @page.title
+ end
+
+private
+
+ def find_wiki
+ @project = Project.find(params[:id])
+ @wiki = @project.wiki
+ render_404 unless @wiki
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ # Finds the requested page and returns a 404 error if it doesn't exist
+ def find_existing_page
+ @page = @wiki.find_page(params[:page])
+ render_404 if @page.nil?
+ end
+
+ # Returns true if the current user is allowed to edit the page, otherwise false
+ def editable?(page = @page)
+ page.editable_by?(User.current)
+ end
+
+ # Returns the default content of a new wiki page
+ def initial_page_content(page)
+ helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
+ extend helper unless self.instance_of?(helper)
+ helper.instance_method(:initial_page_content).bind(self).call(page)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class WikisController < ApplicationController
+ menu_item :settings
+ before_filter :find_project, :authorize
+
+ # Create or update a project's wiki
+ def edit
+ @wiki = @project.wiki || Wiki.new(:project => @project)
+ @wiki.attributes = params[:wiki]
+ @wiki.save if request.post?
+ render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
+ end
+
+ # Delete a project's wiki
+ def destroy
+ if request.post? && params[:confirm] && @project.wiki
+ @project.wiki.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
+ end
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 WorkflowsController < ApplicationController
+ before_filter :require_admin
+
+ def index
+ @workflow_counts = Workflow.count_by_tracker_and_role
+ end
+
+ def edit
+ @role = Role.find_by_id(params[:role_id])
+ @tracker = Tracker.find_by_id(params[:tracker_id])
+
+ if request.post?
+ Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
+ (params[:issue_status] || []).each { |old, news|
+ news.each { |new|
+ @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
+ }
+ }
+ if @role.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
+ end
+ end
+ @roles = Role.find(:all, :order => 'builtin, position')
+ @trackers = Tracker.find(:all, :order => 'position')
+ @statuses = IssueStatus.find(:all, :order => 'position')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module AccountHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module AdminHelper
+ def project_status_options_for_select(selected)
+ options_for_select([[l(:label_all), ''],
+ [l(:status_active), 1]], selected)
+ end
+
+ def css_project_classes(project)
+ s = 'project'
+ s << ' root' if project.root?
+ s << ' child' if project.child?
+ s << (project.leaf? ? ' leaf' : ' parent')
+ s
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 'coderay'
+require 'coderay/helpers/file_type'
+require 'forwardable'
+require 'cgi'
+
+module ApplicationHelper
+ include Redmine::WikiFormatting::Macros::Definitions
+ include Redmine::I18n
+ include GravatarHelper::PublicMethods
+
+ extend Forwardable
+ def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
+
+ # Return true if user is authorized for controller/action, otherwise false
+ def authorize_for(controller, action)
+ User.current.allowed_to?({:controller => controller, :action => action}, @project)
+ end
+
+ # Display a link if user is authorized
+ def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
+ link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
+ end
+
+ # Display a link to remote if user is authorized
+ def link_to_remote_if_authorized(name, options = {}, html_options = nil)
+ url = options[:url] || {}
+ link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
+ end
+
+ # Displays a link to user's account page if active
+ def link_to_user(user, options={})
+ if user.is_a?(User)
+ name = h(user.name(options[:format]))
+ if user.active?
+ link_to name, :controller => 'users', :action => 'show', :id => user
+ else
+ name
+ end
+ else
+ h(user.to_s)
+ end
+ end
+
+ # Displays a link to +issue+ with its subject.
+ # Examples:
+ #
+ # link_to_issue(issue) # => Defect #6: This is the subject
+ # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
+ # link_to_issue(issue, :subject => false) # => Defect #6
+ #
+ def link_to_issue(issue, options={})
+ title = nil
+ subject = nil
+ if options[:subject] == false
+ title = truncate(issue.subject, :length => 60)
+ else
+ subject = issue.subject
+ if options[:truncate]
+ subject = truncate(subject, :length => options[:truncate])
+ end
+ end
+ s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
+ :class => issue.css_classes,
+ :title => title
+ s << ": #{h subject}" if subject
+ s
+ end
+
+ # Generates a link to an attachment.
+ # Options:
+ # * :text - Link text (default to attachment filename)
+ # * :download - Force download (default: false)
+ def link_to_attachment(attachment, options={})
+ text = options.delete(:text) || attachment.filename
+ action = options.delete(:download) ? 'download' : 'show'
+
+ link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
+ end
+
+ def toggle_link(name, id, options={})
+ onclick = "Element.toggle('#{id}'); "
+ onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
+ onclick << "return false;"
+ link_to(name, "#", :onclick => onclick)
+ end
+
+ def image_to_function(name, function, html_options = {})
+ html_options.symbolize_keys!
+ tag(:input, html_options.merge({
+ :type => "image", :src => image_path(name),
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
+ }))
+ end
+
+ def prompt_to_remote(name, text, param, url, html_options = {})
+ html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
+ link_to name, {}, html_options
+ end
+
+ def format_activity_title(text)
+ h(truncate_single_line(text, :length => 100))
+ end
+
+ def format_activity_day(date)
+ date == Date.today ? l(:label_today).titleize : format_date(date)
+ end
+
+ def format_activity_description(text)
+ h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
+ end
+
+ def due_date_distance_in_words(date)
+ if date
+ l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
+ end
+ end
+
+ def render_page_hierarchy(pages, node=nil)
+ content = ''
+ if pages[node]
+ content << "<ul class=\"pages-hierarchy\">\n"
+ pages[node].each do |page|
+ content << "<li>"
+ content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
+ :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
+ content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
+ content << "</li>\n"
+ end
+ content << "</ul>\n"
+ end
+ content
+ end
+
+ # Renders flash messages
+ def render_flash_messages
+ s = ''
+ flash.each do |k,v|
+ s << content_tag('div', v, :class => "flash #{k}")
+ end
+ s
+ end
+
+ # Renders tabs and their content
+ def render_tabs(tabs)
+ if tabs.any?
+ render :partial => 'common/tabs', :locals => {:tabs => tabs}
+ else
+ content_tag 'p', l(:label_no_data), :class => "nodata"
+ end
+ end
+
+ # Renders the project quick-jump box
+ def render_project_jump_box
+ # Retrieve them now to avoid a COUNT query
+ projects = User.current.projects.all
+ if projects.any?
+ s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
+ "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
+ '<option value="" disabled="disabled">---</option>'
+ s << project_tree_options_for_select(projects, :selected => @project) do |p|
+ { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
+ end
+ s << '</select>'
+ s
+ end
+ end
+
+ def project_tree_options_for_select(projects, options = {})
+ s = ''
+ project_tree(projects) do |project, level|
+ name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '')
+ tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
+ tag_options.merge!(yield(project)) if block_given?
+ s << content_tag('option', name_prefix + h(project), tag_options)
+ end
+ s
+ end
+
+ # Yields the given block for each project with its level in the tree
+ def 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
+
+ def project_nested_ul(projects, &block)
+ s = ''
+ if projects.any?
+ ancestors = []
+ projects.sort_by(&:lft).each do |project|
+ if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
+ s << "<ul>\n"
+ else
+ ancestors.pop
+ s << "</li>"
+ while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
+ ancestors.pop
+ s << "</ul></li>\n"
+ end
+ end
+ s << "<li>"
+ s << yield(project).to_s
+ ancestors << project
+ end
+ s << ("</li></ul>\n" * ancestors.size)
+ end
+ s
+ end
+
+ def principals_check_box_tags(name, principals)
+ s = ''
+ principals.sort.each do |principal|
+ s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
+ end
+ s
+ end
+
+ # Truncates and returns the string as a single line
+ def truncate_single_line(string, *args)
+ truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
+ end
+
+ def html_hours(text)
+ text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
+ end
+
+ def authoring(created, author, options={})
+ l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
+ end
+
+ def time_tag(time)
+ text = distance_of_time_in_words(Time.now, time)
+ if @project
+ link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
+ else
+ content_tag('acronym', text, :title => format_time(time))
+ end
+ end
+
+ def syntax_highlight(name, content)
+ type = CodeRay::FileType[name]
+ type ? CodeRay.scan(content, type).html : h(content)
+ end
+
+ def to_path_param(path)
+ path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
+ end
+
+ def pagination_links_full(paginator, count=nil, options={})
+ page_param = options.delete(:page_param) || :page
+ url_param = params.dup
+ # don't reuse query params if filters are present
+ url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
+
+ html = ''
+ if paginator.current.previous
+ html << link_to_remote_content_update('« ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
+ end
+
+ html << (pagination_links_each(paginator, options) do |n|
+ link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
+ end || '')
+
+ if paginator.current.next
+ html << ' ' + link_to_remote_content_update((l(:label_next) + ' »'), url_param.merge(page_param => paginator.current.next))
+ end
+
+ unless count.nil?
+ html << [
+ " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
+ per_page_links(paginator.items_per_page)
+ ].compact.join(' | ')
+ end
+
+ html
+ end
+
+ def per_page_links(selected=nil)
+ url_param = params.dup
+ url_param.clear if url_param.has_key?(:set_filter)
+
+ links = Setting.per_page_options_array.collect do |n|
+ n == selected ? n : link_to_remote(n, {:update => "content",
+ :url => params.dup.merge(:per_page => n),
+ :method => :get},
+ {:href => url_for(url_param.merge(:per_page => n))})
+ end
+ links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
+ end
+
+ def reorder_links(name, url)
+ link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
+ link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
+ link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
+ link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
+ end
+
+ def breadcrumb(*args)
+ elements = args.flatten
+ elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil
+ end
+
+ def other_formats_links(&block)
+ concat('<p class="other-formats">' + l(:label_export_to))
+ yield Redmine::Views::OtherFormatsBuilder.new(self)
+ concat('</p>')
+ end
+
+ def page_header_title
+ if @project.nil? || @project.new_record?
+ h(Setting.app_title)
+ else
+ b = []
+ ancestors = (@project.root? ? [] : @project.ancestors.visible)
+ if ancestors.any?
+ root = ancestors.shift
+ b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
+ if ancestors.size > 2
+ b << '…'
+ ancestors = ancestors[-2, 2]
+ end
+ b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
+ end
+ b << h(@project)
+ b.join(' » ')
+ end
+ end
+
+ def html_title(*args)
+ if args.empty?
+ title = []
+ title << @project.name if @project
+ title += @html_title if @html_title
+ title << Setting.app_title
+ title.select {|t| !t.blank? }.join(' - ')
+ else
+ @html_title ||= []
+ @html_title += args
+ end
+ end
+
+ def accesskey(s)
+ Redmine::AccessKeys.key_for s
+ end
+
+ # Formats text according to system settings.
+ # 2 ways to call this method:
+ # * with a String: textilizable(text, options)
+ # * with an object and one of its attribute: textilizable(issue, :description, options)
+ def textilizable(*args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ case args.size
+ when 1
+ obj = options[:object]
+ text = args.shift
+ when 2
+ obj = args.shift
+ text = obj.send(args.shift).to_s
+ else
+ raise ArgumentError, 'invalid arguments to textilizable'
+ end
+ return '' if text.blank?
+
+ only_path = options.delete(:only_path) == false ? false : true
+
+ # when using an image link, try to use an attachment, if possible
+ attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
+
+ if attachments
+ attachments = attachments.sort_by(&:created_on).reverse
+ text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
+ style = $1
+ filename = $6.downcase
+ # search for the picture in attachments
+ if found = attachments.detect { |att| att.filename.downcase == filename }
+ image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
+ desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
+ alt = desc.blank? ? nil : "(#{desc})"
+ "!#{style}#{image_url}#{alt}!"
+ else
+ m
+ end
+ end
+ end
+
+ text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
+
+ # different methods for formatting wiki links
+ case options[:wiki_links]
+ when :local
+ # used for local links to html files
+ format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
+ when :anchor
+ # used for single-file wiki export
+ format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
+ else
+ format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
+ end
+
+ project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
+
+ # Wiki links
+ #
+ # Examples:
+ # [[mypage]]
+ # [[mypage|mytext]]
+ # wiki links can refer other project wikis, using project name or identifier:
+ # [[project:]] -> wiki starting page
+ # [[project:|mytext]]
+ # [[project:mypage]]
+ # [[project:mypage|mytext]]
+ text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
+ link_project = project
+ esc, all, page, title = $1, $2, $3, $5
+ if esc.nil?
+ if page =~ /^([^\:]+)\:(.*)$/
+ link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
+ page = $2
+ title ||= $1 if page.blank?
+ end
+
+ if link_project && link_project.wiki
+ # extract anchor
+ anchor = nil
+ if page =~ /^(.+?)\#(.+)$/
+ page, anchor = $1, $2
+ end
+ # check if page exists
+ wiki_page = link_project.wiki.find_page(page)
+ link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
+ :class => ('wiki-page' + (wiki_page ? '' : ' new')))
+ else
+ # project or wiki doesn't exist
+ all
+ end
+ else
+ all
+ end
+ end
+
+ # Redmine links
+ #
+ # Examples:
+ # Issues:
+ # #52 -> Link to issue #52
+ # Changesets:
+ # r52 -> Link to revision 52
+ # commit:a85130f -> Link to scmid starting with a85130f
+ # Documents:
+ # document#17 -> Link to document with id 17
+ # document:Greetings -> Link to the document with title "Greetings"
+ # document:"Some document" -> Link to the document with title "Some document"
+ # Versions:
+ # version#3 -> Link to version with id 3
+ # version:1.0.0 -> Link to version named "1.0.0"
+ # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
+ # Attachments:
+ # attachment:file.zip -> Link to the attachment of the current object named file.zip
+ # Source files:
+ # source:some/file -> Link to the file located at /some/file in the project's repository
+ # source:some/file@52 -> Link to the file's revision 52
+ # source:some/file#L120 -> Link to line 120 of the file
+ # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
+ # export:some/file -> Force the download of the file
+ # Forum messages:
+ # message#1218 -> Link to message with id 1218
+ text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
+ leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
+ link = nil
+ if esc.nil?
+ if prefix.nil? && sep == 'r'
+ if project && (changeset = project.changesets.find_by_revision(oid))
+ link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
+ :class => 'changeset',
+ :title => truncate_single_line(changeset.comments, :length => 100))
+ end
+ elsif sep == '#'
+ oid = oid.to_i
+ case prefix
+ when nil
+ if issue = Issue.visible.find_by_id(oid, :include => :status)
+ link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
+ :class => issue.css_classes,
+ :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
+ end
+ when 'document'
+ if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
+ link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
+ :class => 'document'
+ end
+ when 'version'
+ if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
+ link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
+ :class => 'version'
+ end
+ when 'message'
+ if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
+ link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
+ :controller => 'messages',
+ :action => 'show',
+ :board_id => message.board,
+ :id => message.root,
+ :anchor => (message.parent ? "message-#{message.id}" : nil)},
+ :class => 'message'
+ end
+ end
+ elsif sep == ':'
+ # removes the double quotes if any
+ name = oid.gsub(%r{^"(.*)"$}, "\\1")
+ case prefix
+ when 'document'
+ if project && document = project.documents.find_by_title(name)
+ link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
+ :class => 'document'
+ end
+ when 'version'
+ if project && version = project.versions.find_by_name(name)
+ link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
+ :class => 'version'
+ end
+ when 'commit'
+ if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
+ link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
+ :class => 'changeset',
+ :title => truncate_single_line(changeset.comments, :length => 100)
+ end
+ when 'source', 'export'
+ if project && project.repository
+ name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
+ path, rev, anchor = $1, $3, $5
+ link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
+ :path => to_path_param(path),
+ :rev => rev,
+ :anchor => anchor,
+ :format => (prefix == 'export' ? 'raw' : nil)},
+ :class => (prefix == 'export' ? 'source download' : 'source')
+ end
+ when 'attachment'
+ if attachments && attachment = attachments.detect {|a| a.filename == name }
+ link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
+ :class => 'attachment'
+ end
+ end
+ end
+ end
+ leading + (link || "#{prefix}#{sep}#{oid}")
+ end
+
+ text
+ end
+
+ # Same as Rails' simple_format helper without using paragraphs
+ def simple_format_without_paragraph(text)
+ text.to_s.
+ gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
+ gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
+ gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
+ end
+
+ def lang_options_for_select(blank=true)
+ (blank ? [["(auto)", ""]] : []) +
+ valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
+ end
+
+ def label_tag_for(name, option_tags = nil, options = {})
+ label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
+ content_tag("label", label_text)
+ end
+
+ def labelled_tabular_form_for(name, object, options, &proc)
+ options[:html] ||= {}
+ options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
+ form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
+ end
+
+ def back_url_hidden_field_tag
+ back_url = params[:back_url] || request.env['HTTP_REFERER']
+ back_url = CGI.unescape(back_url.to_s)
+ hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
+ end
+
+ def check_all_links(form_name)
+ link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
+ " | " +
+ link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
+ end
+
+ def progress_bar(pcts, options={})
+ pcts = [pcts, pcts] unless pcts.is_a?(Array)
+ pcts = pcts.collect(&:round)
+ pcts[1] = pcts[1] - pcts[0]
+ pcts << (100 - pcts[1] - pcts[0])
+ width = options[:width] || '100px;'
+ legend = options[:legend] || ''
+ content_tag('table',
+ content_tag('tr',
+ (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
+ (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
+ (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
+ ), :class => 'progress', :style => "width: #{width};") +
+ content_tag('p', legend, :class => 'pourcent')
+ end
+
+ def context_menu_link(name, url, options={})
+ options[:class] ||= ''
+ if options.delete(:selected)
+ options[:class] << ' icon-checked disabled'
+ options[:disabled] = true
+ end
+ if options.delete(:disabled)
+ options.delete(:method)
+ options.delete(:confirm)
+ options.delete(:onclick)
+ options[:class] << ' disabled'
+ url = '#'
+ end
+ link_to name, url, options
+ end
+
+ def calendar_for(field_id)
+ include_calendar_headers_tags
+ image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
+ javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
+ end
+
+ def include_calendar_headers_tags
+ unless @calendar_headers_tags_included
+ @calendar_headers_tags_included = true
+ content_for :header_tags do
+ javascript_include_tag('calendar/calendar') +
+ javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
+ javascript_include_tag('calendar/calendar-setup') +
+ stylesheet_link_tag('calendar')
+ end
+ end
+ end
+
+ def content_for(name, content = nil, &block)
+ @has_content ||= {}
+ @has_content[name] = true
+ super(name, content, &block)
+ end
+
+ def has_content?(name)
+ (@has_content && @has_content[name]) || false
+ end
+
+ # Returns the avatar image tag for the given +user+ if avatars are enabled
+ # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
+ def avatar(user, options = { })
+ if Setting.gravatar_enabled?
+ options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
+ email = nil
+ if user.respond_to?(:mail)
+ email = user.mail
+ elsif user.to_s =~ %r{<(.+?)>}
+ email = $1
+ end
+ return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
+ end
+ end
+
+ private
+
+ def wiki_helper
+ helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
+ extend helper
+ return self
+ end
+
+ def link_to_remote_content_update(text, url_params)
+ link_to_remote(text,
+ {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
+ {:href => url_for(:params => url_params)}
+ )
+ end
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 AttachmentsHelper
+ # Displays view/delete links to the attachments of the given object
+ # Options:
+ # :author -- author names are not displayed if set to false
+ def link_to_attachments(container, options = {})
+ options.assert_valid_keys(:author)
+
+ if container.attachments.any?
+ options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
+ render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
+ end
+ end
+
+ def to_utf8(str)
+ str
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module AuthSourcesHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module BoardsHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module CustomFieldsHelper
+
+ def custom_fields_tabs
+ tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
+ {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
+ {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
+ {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
+ {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
+ {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
+ {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
+ {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
+ {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
+ ]
+ end
+
+ # Return custom field html tag corresponding to its format
+ def custom_field_tag(name, custom_value)
+ custom_field = custom_value.custom_field
+ field_name = "#{name}[custom_field_values][#{custom_field.id}]"
+ field_id = "#{name}_custom_field_values_#{custom_field.id}"
+
+ case custom_field.field_format
+ when "date"
+ text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
+ calendar_for(field_id)
+ when "text"
+ text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
+ when "bool"
+ hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
+ when "list"
+ blank_option = custom_field.is_required? ?
+ (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
+ '<option></option>'
+ select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
+ else
+ text_field_tag(field_name, custom_value.value, :id => field_id)
+ end
+ end
+
+ # Return custom field label tag
+ def custom_field_label_tag(name, custom_value)
+ content_tag "label", custom_value.custom_field.name +
+ (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
+ :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
+ :class => (custom_value.errors.empty? ? nil : "error" )
+ end
+
+ # Return custom field tag with its label tag
+ def custom_field_tag_with_label(name, custom_value)
+ custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
+ end
+
+ # Return a string used to display a custom value
+ def show_value(custom_value)
+ return "" unless custom_value
+ format_value(custom_value.value, custom_value.custom_field.field_format)
+ end
+
+ # Return a string used to display a custom value
+ def format_value(value, field_format)
+ return "" unless value && !value.empty?
+ case field_format
+ when "date"
+ begin; format_date(value.to_date); rescue; value end
+ when "bool"
+ l(value == "1" ? :general_text_Yes : :general_text_No)
+ else
+ value
+ end
+ end
+
+ # Return an array of custom field formats which can be used in select_tag
+ def custom_field_formats_for_select
+ CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module DocumentsHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module EnumerationsHelper
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module GroupsHelper
+ # Options for the new membership projects combo-box
+ def options_for_membership_project_select(user, projects)
+ options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
+ options << project_tree_options_for_select(projects) do |p|
+ {:disabled => (user.projects.include?(p))}
+ end
+ options
+ end
+
+ def group_settings_tabs
+ tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
+ {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
+ {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
+ ]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module IssueCategoriesHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 IssueRelationsHelper
+ def collection_for_relation_type_select
+ values = IssueRelation::TYPES
+ values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module IssueStatusesHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module IssuesHelper
+ include ApplicationHelper
+
+ def render_issue_tooltip(issue)
+ @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)
+
+ link_to_issue(issue) + "<br /><br />" +
+ "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
+ "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
+ "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
+ "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
+ end
+
+ def render_custom_fields_rows(issue)
+ return if issue.custom_field_values.empty?
+ ordered_values = []
+ half = (issue.custom_field_values.size / 2.0).ceil
+ half.times do |i|
+ ordered_values << issue.custom_field_values[i]
+ ordered_values << issue.custom_field_values[i + half]
+ end
+ s = "<tr>\n"
+ n = 0
+ ordered_values.compact.each do |value|
+ s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
+ s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
+ n += 1
+ end
+ s << "</tr>\n"
+ s
+ end
+
+ def sidebar_queries
+ unless @sidebar_queries
+ # User can see public queries and his own queries
+ visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
+ # Project specific queries and global queries
+ visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
+ @sidebar_queries = Query.find(:all,
+ :select => 'id, name',
+ :order => "name ASC",
+ :conditions => visible.conditions)
+ end
+ @sidebar_queries
+ end
+
+ def show_detail(detail, no_html=false)
+ case detail.property
+ when 'attr'
+ label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).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'
+ p = Project.find_by_id(detail.value) and value = p.name if detail.value
+ p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
+ when 'status_id'
+ s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
+ s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
+ when 'tracker_id'
+ t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
+ t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
+ when 'assigned_to_id'
+ u = User.find_by_id(detail.value) and value = u.name if detail.value
+ u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
+ when 'priority_id'
+ e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
+ e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
+ when 'category_id'
+ c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
+ c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
+ when 'fixed_version_id'
+ v = Version.find_by_id(detail.value) and value = v.name if detail.value
+ v = Version.find_by_id(detail.old_value) and old_value = v.name if 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?
+ end
+ when 'cf'
+ custom_field = CustomField.find_by_id(detail.prop_key)
+ if custom_field
+ label = custom_field.name
+ value = format_value(detail.value, custom_field.field_format) if detail.value
+ old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
+ end
+ when 'attachment'
+ label = l(:label_attachment)
+ end
+ call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
+
+ label ||= detail.prop_key
+ value ||= detail.value
+ old_value ||= detail.old_value
+
+ unless no_html
+ label = content_tag('strong', label)
+ old_value = content_tag("i", h(old_value)) if detail.old_value
+ old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
+ if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
+ # Link to the attachment if it has not been removed
+ value = link_to_attachment(a)
+ else
+ value = content_tag("i", h(value)) if value
+ end
+ end
+
+ if !detail.value.blank?
+ case detail.property
+ when 'attr', 'cf'
+ if !detail.old_value.blank?
+ l(:text_journal_changed, :label => label, :old => old_value, :new => value)
+ else
+ l(:text_journal_set_to, :label => label, :value => value)
+ end
+ when 'attachment'
+ l(:text_journal_added, :label => label, :value => value)
+ end
+ else
+ l(:text_journal_deleted, :label => label, :old => old_value)
+ end
+ end
+
+ def issues_to_csv(issues, project = nil)
+ ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ decimal_separator = l(:general_csv_decimal_separator)
+ export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
+ # csv header fields
+ headers = [ "#",
+ l(:field_status),
+ l(:field_project),
+ l(:field_tracker),
+ l(:field_priority),
+ l(:field_subject),
+ l(:field_assigned_to),
+ l(:field_category),
+ l(:field_fixed_version),
+ l(:field_author),
+ l(:field_start_date),
+ l(:field_due_date),
+ l(:field_done_ratio),
+ l(:field_estimated_hours),
+ l(:field_created_on),
+ l(:field_updated_on)
+ ]
+ # Export project custom fields if project is given
+ # otherwise export custom fields marked as "For all projects"
+ custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
+ custom_fields.each {|f| headers << f.name}
+ # Description in the last column
+ headers << l(:field_description)
+ csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
+ # csv lines
+ issues.each do |issue|
+ fields = [issue.id,
+ issue.status.name,
+ issue.project.name,
+ issue.tracker.name,
+ issue.priority.name,
+ issue.subject,
+ issue.assigned_to,
+ issue.category,
+ issue.fixed_version,
+ issue.author.name,
+ format_date(issue.start_date),
+ format_date(issue.due_date),
+ issue.done_ratio,
+ issue.estimated_hours.to_s.gsub('.', decimal_separator),
+ format_time(issue.created_on),
+ format_time(issue.updated_on)
+ ]
+ custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
+ fields << issue.description
+ csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
+ end
+ end
+ export
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 JournalsHelper
+ def render_notes(journal, options={})
+ content = ''
+ editable = journal.editable_by?(User.current)
+ links = []
+ if !journal.notes.blank?
+ links << link_to_remote(image_tag('comment.png'),
+ { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
+ :title => l(:button_quote)) if options[:reply_links]
+ links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
+ { :controller => 'journals', :action => 'edit', :id => journal },
+ :title => l(:button_edit)) if editable
+ end
+ content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
+ content << textilizable(journal, :notes)
+ css_classes = "wiki"
+ css_classes << " editable" if editable
+ css_classes << " gravatar-margin" if Setting.gravatar_enabled?
+ content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
+ end
+
+ def link_to_in_place_notes_editor(text, field_id, url, options={})
+ onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
+ link_to text, '#', options.merge(:onclick => onclick)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 MailHandlerHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module MembersHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 MessagesHelper
+
+ def link_to_message(message)
+ return '' unless message
+ link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
+ :action => 'show',
+ :board_id => message.board_id,
+ :id => message.root,
+ :anchor => (message.parent_id ? "message-#{message.id}" : nil)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module MyHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module NewsHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module ProjectsHelper
+ def link_to_version(version, options = {})
+ return '' unless version && version.is_a?(Version)
+ link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
+ end
+
+ def project_settings_tabs
+ tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
+ {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
+ {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
+ {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
+ {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
+ {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
+ {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
+ {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
+ {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
+ ]
+ tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
+ end
+
+ def parent_project_select_tag(project)
+ options = '<option></option>' + project_tree_options_for_select(project.allowed_parents, :selected => project.parent)
+ content_tag('select', options, :name => 'project[parent_id]')
+ end
+
+ # Renders a tree of projects as a nested set of unordered lists
+ # The given collection may be a subset of the whole project tree
+ # (eg. some intermediate nodes are private and can not be seen)
+ def render_project_hierarchy(projects)
+ s = ''
+ if projects.any?
+ ancestors = []
+ projects.each do |project|
+ if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
+ s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
+ else
+ ancestors.pop
+ s << "</li>"
+ while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
+ ancestors.pop
+ s << "</ul></li>\n"
+ end
+ end
+ classes = (ancestors.empty? ? 'root' : 'child')
+ s << "<li class='#{classes}'><div class='#{classes}'>" +
+ link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
+ s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
+ s << "</div>\n"
+ ancestors << project
+ end
+ s << ("</li></ul>\n" * ancestors.size)
+ end
+ s
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module QueriesHelper
+
+ def operators_for_select(filter_type)
+ Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
+ end
+
+ def column_header(column)
+ column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
+ :default_order => column.default_order) :
+ content_tag('th', column.caption)
+ end
+
+ def column_value(column, issue)
+ if column.is_a?(QueryCustomFieldColumn)
+ cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
+ show_value(cv)
+ else
+ value = issue.send(column.name)
+ end
+ end
+
+ def column_content(column, issue)
+ if column.is_a?(QueryCustomFieldColumn)
+ cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
+ show_value(cv)
+ else
+ value = issue.send(column.name)
+ if value.is_a?(Date)
+ format_date(value)
+ elsif value.is_a?(Time)
+ format_time(value)
+ else
+ case column.name
+ when :subject
+ h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
+ link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
+ when :project
+ link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
+ when :assigned_to
+ link_to_user value
+ when :author
+ link_to_user value
+ when :done_ratio
+ progress_bar(value, :width => '80px')
+ when :fixed_version
+ link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
+ else
+ h(value)
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module ReportsHelper
+
+ def aggregate(data, criteria)
+ a = 0
+ data.each { |row|
+ match = 1
+ criteria.each { |k, v|
+ match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t"))
+ } unless criteria.nil?
+ a = a + row["total"].to_i if match == 1
+ } unless data.nil?
+ a
+ end
+
+ def aggregate_link(data, criteria, *args)
+ a = aggregate data, criteria
+ a > 0 ? link_to(a, *args) : '-'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'iconv'
+
+module RepositoriesHelper
+ def format_revision(txt)
+ txt.to_s[0,8]
+ end
+
+ def truncate_at_line_break(text, length = 255)
+ if text
+ text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
+ end
+ end
+
+ def render_properties(properties)
+ unless properties.nil? || properties.empty?
+ content = ''
+ properties.keys.sort.each do |property|
+ content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
+ end
+ content_tag('ul', content, :class => 'properties')
+ end
+ end
+
+ def render_changeset_changes
+ changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
+ case change.action
+ when 'A'
+ # Detects moved/copied files
+ if !change.from_path.blank?
+ change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
+ end
+ change
+ when 'D'
+ @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
+ else
+ change
+ end
+ end.compact
+
+ tree = { }
+ changes.each do |change|
+ p = tree
+ dirs = change.path.to_s.split('/').select {|d| !d.blank?}
+ dirs.each do |dir|
+ p[:s] ||= {}
+ p = p[:s]
+ p[dir] ||= {}
+ p = p[dir]
+ end
+ p[:c] = change
+ end
+
+ render_changes_tree(tree[:s])
+ end
+
+ def render_changes_tree(tree)
+ return '' if tree.nil?
+
+ output = ''
+ output << '<ul>'
+ tree.keys.sort.each do |file|
+ s = !tree[file][:s].nil?
+ c = tree[file][:c]
+
+ style = 'change'
+ style << ' folder' if s
+ style << " change-#{c.action}" if c
+
+ text = h(file)
+ unless c.nil?
+ path_param = to_path_param(@repository.relative_path(c.path))
+ text = link_to(text, :controller => 'repositories',
+ :action => 'entry',
+ :id => @project,
+ :path => path_param,
+ :rev => @changeset.revision) unless s || c.action == 'D'
+ text << " - #{c.revision}" unless c.revision.blank?
+ text << ' (' + link_to('diff', :controller => 'repositories',
+ :action => 'diff',
+ :id => @project,
+ :path => path_param,
+ :rev => @changeset.revision) + ') ' if c.action == 'M'
+ text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
+ end
+ output << "<li class='#{style}'>#{text}</li>"
+ output << render_changes_tree(tree[file][:s]) if s
+ end
+ output << '</ul>'
+ output
+ end
+
+ def to_utf8(str)
+ return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
+ @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
+ @encodings.each do |encoding|
+ begin
+ return Iconv.conv('UTF-8', encoding, str)
+ rescue Iconv::Failure
+ # do nothing here and try the next encoding
+ end
+ end
+ str
+ end
+
+ def repository_field_tags(form, repository)
+ method = repository.class.name.demodulize.underscore + "_field_tags"
+ send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
+ end
+
+ def scm_select_tag(repository)
+ scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
+ REDMINE_SUPPORTED_SCM.each do |scm|
+ scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
+ end
+
+ select_tag('repository_scm',
+ options_for_select(scm_options, repository.class.name.demodulize),
+ :disabled => (repository && !repository.new_record?),
+ :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
+ )
+ end
+
+ def with_leading_slash(path)
+ path.to_s.starts_with?('/') ? path : "/#{path}"
+ end
+
+ def without_leading_slash(path)
+ path.gsub(%r{^/+}, '')
+ end
+
+ def subversion_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
+ '<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
+ content_tag('p', form.text_field(:login, :size => 30)) +
+ content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
+ :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
+ :onfocus => "this.value=''; this.name='repository[password]';",
+ :onchange => "this.name='repository[password]';"))
+ end
+
+ def darcs_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
+ end
+
+ def mercurial_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+ end
+
+ def git_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+ end
+
+ def cvs_field_tags(form, repository)
+ content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
+ content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
+ end
+
+ def bazaar_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
+ end
+
+ def filesystem_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module RolesHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module SearchHelper
+ def highlight_tokens(text, tokens)
+ return text unless text && tokens && !tokens.empty?
+ re_tokens = tokens.collect {|t| Regexp.escape(t)}
+ regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
+ result = ''
+ text.split(regexp).each_with_index do |words, i|
+ if result.length > 1200
+ # maximum length of the preview reached
+ result << '...'
+ break
+ end
+ words = words.mb_chars
+ if i.even?
+ result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
+ else
+ t = (tokens.index(words.downcase) || 0) % 4
+ result << content_tag('span', h(words), :class => "highlight token-#{t}")
+ end
+ end
+ result
+ end
+
+ def type_label(t)
+ l("label_#{t.singularize}_plural")
+ end
+
+ def project_select_tag
+ options = [[l(:label_project_all), 'all']]
+ options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
+ options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
+ options << [@project.name, ''] unless @project.nil?
+ select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
+ end
+
+ def render_results_by_type(results_by_type)
+ links = []
+ # Sorts types by results count
+ results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
+ c = results_by_type[t]
+ next if c == 0
+ text = "#{type_label(t)} (#{c})"
+ links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
+ end
+ ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 SettingsHelper
+ def administration_settings_tabs
+ tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
+ {:name => 'display', :partial => 'settings/display', :label => :label_display},
+ {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
+ {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
+ {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
+ {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
+ {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
+ {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
+ ]
+ end
+end
--- /dev/null
+# Helpers to sort tables using clickable column headers.
+#
+# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
+# Jean-Philippe Lang, 2009
+# License: This source code is released under the MIT license.
+#
+# - Consecutive clicks toggle the column's sort order.
+# - Sort state is maintained by a session hash entry.
+# - CSS classes identify sort column and state.
+# - Typically used in conjunction with the Pagination module.
+#
+# Example code snippets:
+#
+# Controller:
+#
+# helper :sort
+# include SortHelper
+#
+# def list
+# sort_init 'last_name'
+# sort_update %w(first_name last_name)
+# @items = Contact.find_all nil, sort_clause
+# end
+#
+# Controller (using Pagination module):
+#
+# helper :sort
+# include SortHelper
+#
+# def list
+# sort_init 'last_name'
+# sort_update %w(first_name last_name)
+# @contact_pages, @items = paginate :contacts,
+# :order_by => sort_clause,
+# :per_page => 10
+# end
+#
+# View (table header in list.rhtml):
+#
+# <thead>
+# <tr>
+# <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
+# <%= sort_header_tag('last_name', :caption => 'Name') %>
+# <%= sort_header_tag('phone') %>
+# <%= sort_header_tag('address', :width => 200) %>
+# </tr>
+# </thead>
+#
+# - Introduces instance variables: @sort_default, @sort_criteria
+# - Introduces param :sort
+#
+
+module SortHelper
+ class SortCriteria
+
+ def initialize
+ @criteria = []
+ end
+
+ def available_criteria=(criteria)
+ unless criteria.is_a?(Hash)
+ criteria = criteria.inject({}) {|h,k| h[k] = k; h}
+ end
+ @available_criteria = criteria
+ end
+
+ def from_param(param)
+ @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
+ normalize!
+ end
+
+ def criteria=(arg)
+ @criteria = arg
+ normalize!
+ end
+
+ def to_param
+ @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
+ end
+
+ def to_sql
+ sql = @criteria.collect do |k,o|
+ if s = @available_criteria[k]
+ (o ? s.to_a : s.to_a.collect {|c| "#{c} DESC"}).join(', ')
+ end
+ end.compact.join(', ')
+ sql.blank? ? nil : sql
+ end
+
+ def add!(key, asc)
+ @criteria.delete_if {|k,o| k == key}
+ @criteria = [[key, asc]] + @criteria
+ normalize!
+ end
+
+ def add(*args)
+ r = self.class.new.from_param(to_param)
+ r.add!(*args)
+ r
+ end
+
+ def first_key
+ @criteria.first && @criteria.first.first
+ end
+
+ def first_asc?
+ @criteria.first && @criteria.first.last
+ end
+
+ def empty?
+ @criteria.empty?
+ end
+
+ private
+
+ def normalize!
+ @criteria ||= []
+ @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
+ @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
+ @criteria.slice!(3)
+ self
+ end
+ end
+
+ def sort_name
+ controller_name + '_' + action_name + '_sort'
+ end
+
+ # Initializes the default sort.
+ # Examples:
+ #
+ # sort_init 'name'
+ # sort_init 'id', 'desc'
+ # sort_init ['name', ['id', 'desc']]
+ # sort_init [['name', 'desc'], ['id', 'desc']]
+ #
+ def sort_init(*args)
+ case args.size
+ when 1
+ @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
+ when 2
+ @sort_default = [[args.first, args.last]]
+ else
+ raise ArgumentError
+ end
+ end
+
+ # Updates the sort state. Call this in the controller prior to calling
+ # sort_clause.
+ # - criteria can be either an array or a hash of allowed keys
+ #
+ def sort_update(criteria)
+ @sort_criteria = SortCriteria.new
+ @sort_criteria.available_criteria = criteria
+ @sort_criteria.from_param(params[:sort] || session[sort_name])
+ @sort_criteria.criteria = @sort_default if @sort_criteria.empty?
+ session[sort_name] = @sort_criteria.to_param
+ end
+
+ # Clears the sort criteria session data
+ #
+ def sort_clear
+ session[sort_name] = nil
+ end
+
+ # Returns an SQL sort clause corresponding to the current sort state.
+ # Use this to sort the controller's table items collection.
+ #
+ def sort_clause()
+ @sort_criteria.to_sql
+ end
+
+ # Returns a link which sorts by the named column.
+ #
+ # - column is the name of an attribute in the sorted record collection.
+ # - the optional caption explicitly specifies the displayed link text.
+ # - 2 CSS classes reflect the state of the link: sort and asc or desc
+ #
+ def sort_link(column, caption, default_order)
+ css, order = nil, default_order
+
+ if column.to_s == @sort_criteria.first_key
+ if @sort_criteria.first_asc?
+ css = 'sort asc'
+ order = 'desc'
+ else
+ css = 'sort desc'
+ order = 'asc'
+ end
+ end
+ caption = column.to_s.humanize unless caption
+
+ sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
+ # don't reuse params if filters are present
+ url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
+
+ # Add project_id to url_options
+ url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
+
+ link_to_remote(caption,
+ {:update => "content", :url => url_options, :method => :get},
+ {:href => url_for(url_options),
+ :class => css})
+ end
+
+ # Returns a table header <th> tag with a sort link for the named column
+ # attribute.
+ #
+ # Options:
+ # :caption The displayed link name (defaults to titleized column name).
+ # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
+ #
+ # Other options hash entries generate additional table header tag attributes.
+ #
+ # Example:
+ #
+ # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
+ #
+ def sort_header_tag(column, options = {})
+ caption = options.delete(:caption) || column.to_s.humanize
+ default_order = options.delete(:default_order) || 'asc'
+ options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
+ content_tag('th', sort_link(column, caption, default_order), options)
+ end
+end
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module TimelogHelper
+ include ApplicationHelper
+
+ def render_timelog_breadcrumb
+ links = []
+ links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
+ links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
+ if @issue
+ if @issue.visible?
+ links << link_to_issue(@issue, :subject => false)
+ else
+ links << "##{@issue.id}"
+ end
+ end
+ breadcrumb links
+ end
+
+ # Returns a collection of activities for a select field. time_entry
+ # is optional and will be used to check if the selected TimeEntryActivity
+ # is active.
+ def activity_collection_for_select_options(time_entry=nil, project=nil)
+ project ||= @project
+ if project.nil?
+ activities = TimeEntryActivity.shared.active
+ else
+ activities = project.activities
+ end
+
+ collection = []
+ if time_entry && time_entry.activity && !time_entry.activity.active?
+ collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
+ else
+ collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
+ end
+ activities.each { |a| collection << [a.name, a.id] }
+ collection
+ end
+
+ def select_hours(data, criteria, value)
+ if value.to_s.empty?
+ data.select {|row| row[criteria].blank? }
+ else
+ data.select {|row| row[criteria] == value}
+ end
+ end
+
+ def sum_hours(data)
+ sum = 0
+ data.each do |row|
+ sum += row['hours'].to_f
+ end
+ sum
+ end
+
+ def options_for_period_select(value)
+ options_for_select([[l(:label_all_time), 'all'],
+ [l(:label_today), 'today'],
+ [l(:label_yesterday), 'yesterday'],
+ [l(:label_this_week), 'current_week'],
+ [l(:label_last_week), 'last_week'],
+ [l(:label_last_n_days, 7), '7_days'],
+ [l(:label_this_month), 'current_month'],
+ [l(:label_last_month), 'last_month'],
+ [l(:label_last_n_days, 30), '30_days'],
+ [l(:label_this_year), 'current_year']],
+ value)
+ end
+
+ def entries_to_csv(entries)
+ ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ decimal_separator = l(:general_csv_decimal_separator)
+ custom_fields = TimeEntryCustomField.find(:all)
+ export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
+ # csv header fields
+ headers = [l(:field_spent_on),
+ l(:field_user),
+ l(:field_activity),
+ l(:field_project),
+ l(:field_issue),
+ l(:field_tracker),
+ l(:field_subject),
+ l(:field_hours),
+ l(:field_comments)
+ ]
+ # Export custom fields
+ headers += custom_fields.collect(&:name)
+
+ csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
+ # csv lines
+ entries.each do |entry|
+ fields = [format_date(entry.spent_on),
+ entry.user,
+ entry.activity,
+ entry.project,
+ (entry.issue ? entry.issue.id : nil),
+ (entry.issue ? entry.issue.tracker : nil),
+ (entry.issue ? entry.issue.subject : nil),
+ entry.hours.to_s.gsub('.', decimal_separator),
+ entry.comments
+ ]
+ fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
+
+ csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
+ end
+ end
+ export
+ end
+
+ def format_criteria_value(criteria, value)
+ if value.blank?
+ l(:label_none)
+ elsif k = @available_criterias[criteria][:klass]
+ obj = k.find_by_id(value.to_i)
+ if obj.is_a?(Issue)
+ obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
+ else
+ obj
+ end
+ else
+ format_value(value, @available_criterias[criteria][:format])
+ end
+ end
+
+ def report_to_csv(criterias, periods, hours)
+ export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
+ # Column headers
+ headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
+ headers += periods
+ headers << l(:label_total)
+ csv << headers.collect {|c| to_utf8(c) }
+ # Content
+ report_criteria_to_csv(csv, criterias, periods, hours)
+ # Total row
+ row = [ l(:label_total) ] + [''] * (criterias.size - 1)
+ total = 0
+ periods.each do |period|
+ sum = sum_hours(select_hours(hours, @columns, period.to_s))
+ total += sum
+ row << (sum > 0 ? "%.2f" % sum : '')
+ end
+ row << "%.2f" %total
+ csv << row
+ end
+ export
+ end
+
+ def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
+ hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
+ hours_for_value = select_hours(hours, criterias[level], value)
+ next if hours_for_value.empty?
+ row = [''] * level
+ row << to_utf8(format_criteria_value(criterias[level], value))
+ row += [''] * (criterias.length - level - 1)
+ total = 0
+ periods.each do |period|
+ sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
+ total += sum
+ row << (sum > 0 ? "%.2f" % sum : '')
+ end
+ row << "%.2f" %total
+ csv << row
+
+ if criterias.length > level + 1
+ report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
+ end
+ end
+ end
+
+ def to_utf8(s)
+ @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ begin; @ic.iconv(s.to_s); rescue; s.to_s; end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module TrackersHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+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)
+ end
+
+ # Options for the new membership projects combo-box
+ def options_for_membership_project_select(user, projects)
+ options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
+ options << project_tree_options_for_select(projects) do |p|
+ {:disabled => (user.projects.include?(p))}
+ end
+ options
+ end
+
+ def change_status_link(user)
+ url = {:controller => 'users', :action => 'edit', :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 => :post, :class => 'icon icon-unlock'
+ elsif user.registered?
+ link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
+ elsif user != User.current
+ link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
+ end
+ end
+
+ def user_settings_tabs
+ tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
+ {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural},
+ {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
+ ]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module VersionsHelper
+
+ STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to)
+
+ def render_issue_status_by(version, criteria)
+ criteria ||= 'category'
+ raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria)
+
+ h = Hash.new {|k,v| k[v] = [0, 0]}
+ begin
+ # Total issue count
+ Issue.count(:group => criteria,
+ :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
+ # Open issues count
+ Issue.count(:group => criteria,
+ :include => :status,
+ :conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
+ rescue ActiveRecord::RecordNotFound
+ # When grouping by an association, Rails throws this exception if there's no result (bug)
+ end
+ counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
+ max = counts.collect {|c| c[:total]}.max
+
+ render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
+ end
+
+ def status_by_options_for_select(value)
+ options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 WatchersHelper
+ def watcher_tag(object, user)
+ content_tag("span", watcher_link(object, user), :id => 'watcher')
+ end
+
+ def watcher_link(object, user)
+ return '' unless user && user.logged? && object.respond_to?('watched_by?')
+ watched = object.watched_by?(user)
+ url = {:controller => 'watchers',
+ :action => (watched ? 'unwatch' : 'watch'),
+ :object_type => object.class.to_s.underscore,
+ :object_id => object.id}
+ link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
+ {:url => url},
+ :href => url_for(url),
+ :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
+
+ end
+
+ # Returns a comma separated list of users watching the given object
+ def watchers_list(object)
+ remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
+ object.watcher_users.collect do |user|
+ s = content_tag('span', link_to_user(user), :class => 'user')
+ if remove_allowed
+ url = {:controller => 'watchers',
+ :action => 'destroy',
+ :object_type => object.class.to_s.underscore,
+ :object_id => object.id,
+ :user_id => user}
+ s += ' ' + link_to_remote(image_tag('delete.png'),
+ {:url => url},
+ :href => url_for(url),
+ :style => "vertical-align: middle")
+ end
+ s
+ end.join(",\n")
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module WelcomeHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module WikiHelper
+
+ def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
+ s = ''
+ pages.select {|p| p.parent == parent}.each do |page|
+ attrs = "value='#{page.id}'"
+ attrs << " selected='selected'" if selected == page
+ indent = (level > 0) ? (' ' * level * 2 + '» ') : nil
+
+ s << "<option value='#{page.id}'>#{indent}#{h page.pretty_title}</option>\n" +
+ wiki_page_options_for_select(pages, selected, page, level + 1)
+ end
+ s
+ end
+
+ def html_diff(wdiff)
+ words = wdiff.words.collect{|word| h(word)}
+ words_add = 0
+ words_del = 0
+ dels = 0
+ del_off = 0
+ wdiff.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 << ' ' + h(change[2])
+ words_del += 1
+ end
+ end
+ if add_at
+ words[add_at] = '<span class="diff_in">' + words[add_at]
+ words[add_to] = words[add_to] + '</span>'
+ end
+ if del_at
+ words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
+ dels += 1
+ del_off += words_del
+ words_del = 0
+ end
+ end
+ simple_format_without_paragraph(words.join(' '))
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 WorkflowsHelper
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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/md5"
+
+class Attachment < ActiveRecord::Base
+ belongs_to :container, :polymorphic => true
+ belongs_to :author, :class_name => "User", :foreign_key => "author_id"
+
+ validates_presence_of :container, :filename, :author
+ validates_length_of :filename, :maximum => 255
+ validates_length_of :disk_filename, :maximum => 255
+
+ acts_as_event :title => :filename,
+ :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
+
+ acts_as_activity_provider :type => 'files',
+ :permission => :view_files,
+ :author_key => :author_id,
+ :find_options => {:select => "#{Attachment.table_name}.*",
+ :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
+
+ acts_as_activity_provider :type => 'documents',
+ :permission => :view_documents,
+ :author_key => :author_id,
+ :find_options => {:select => "#{Attachment.table_name}.*",
+ :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
+
+ cattr_accessor :storage_path
+ @@storage_path = "#{RAILS_ROOT}/files"
+
+ def validate
+ if self.filesize > Setting.attachment_max_size.to_i.kilobytes
+ errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
+ end
+ end
+
+ def file=(incoming_file)
+ unless incoming_file.nil?
+ @temp_file = incoming_file
+ if @temp_file.size > 0
+ self.filename = sanitize_filename(@temp_file.original_filename)
+ self.disk_filename = Attachment.disk_filename(filename)
+ self.content_type = @temp_file.content_type.to_s.chomp
+ self.filesize = @temp_file.size
+ end
+ end
+ end
+
+ def file
+ nil
+ end
+
+ # Copies the temporary file to its final location
+ # and computes its MD5 hash
+ def before_save
+ if @temp_file && (@temp_file.size > 0)
+ logger.debug("saving '#{self.diskfile}'")
+ md5 = Digest::MD5.new
+ File.open(diskfile, "wb") do |f|
+ buffer = ""
+ while (buffer = @temp_file.read(8192))
+ f.write(buffer)
+ md5.update(buffer)
+ end
+ end
+ self.digest = md5.hexdigest
+ end
+ # Don't save the content type if it's longer than the authorized length
+ if self.content_type && self.content_type.length > 255
+ self.content_type = nil
+ end
+ end
+
+ # Deletes file on the disk
+ def after_destroy
+ File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
+ end
+
+ # Returns file's location on disk
+ def diskfile
+ "#{@@storage_path}/#{self.disk_filename}"
+ end
+
+ def increment_download
+ increment!(:downloads)
+ end
+
+ def project
+ container.project
+ end
+
+ def visible?(user=User.current)
+ container.attachments_visible?(user)
+ end
+
+ def deletable?(user=User.current)
+ container.attachments_deletable?(user)
+ end
+
+ def image?
+ self.filename =~ /\.(jpe?g|gif|png)$/i
+ end
+
+ def is_text?
+ Redmine::MimeType.is_type?('text', filename)
+ end
+
+ def is_diff?
+ self.filename =~ /\.(patch|diff)$/i
+ end
+
+ # Returns true if the file is readable
+ def readable?
+ File.readable?(diskfile)
+ end
+
+private
+ def sanitize_filename(value)
+ # get only the filename, not the whole path
+ just_filename = value.gsub(/^.*(\\|\/)/, '')
+ # NOTE: File.basename doesn't work right with Windows paths on Unix
+ # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
+
+ # Finally, replace all non alphanumeric, hyphens or periods with underscore
+ @filename = just_filename.gsub(/[^\w\.\-]/,'_')
+ end
+
+ # Returns an ASCII or hashed filename
+ def self.disk_filename(filename)
+ df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
+ if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
+ df << filename
+ else
+ df << Digest::MD5.hexdigest(filename)
+ # keep the extension if any
+ df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
+ end
+ df
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class AuthSource < ActiveRecord::Base
+ has_many :users
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+ validates_length_of :name, :maximum => 60
+
+ def authenticate(login, password)
+ end
+
+ def test_connection
+ end
+
+ def auth_method_name
+ "Abstract"
+ end
+
+ # Try to authenticate a user not yet registered against available sources
+ def self.authenticate(login, password)
+ AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
+ begin
+ logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
+ attrs = source.authenticate(login, password)
+ rescue => e
+ logger.error "Error during authentication: #{e.message}"
+ attrs = nil
+ end
+ return attrs if attrs
+ end
+ return nil
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'net/ldap'
+require 'iconv'
+
+class AuthSourceLdap < AuthSource
+ validates_presence_of :host, :port, :attr_login
+ validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true
+ validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true
+ validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
+ validates_numericality_of :port, :only_integer => true
+
+ before_validation :strip_ldap_attributes
+
+ def after_initialize
+ self.port = 389 if self.port == 0
+ end
+
+ def authenticate(login, password)
+ return nil if login.blank? || password.blank?
+ attrs = []
+ # get user's DN
+ ldap_con = initialize_ldap_con(self.account, self.account_password)
+ login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
+ object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
+ dn = String.new
+ ldap_con.search( :base => self.base_dn,
+ :filter => object_filter & login_filter,
+ # only ask for the DN if on-the-fly registration is disabled
+ :attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
+ dn = entry.dn
+ attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
+ :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
+ :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
+ :auth_source_id => self.id ] if onthefly_register?
+ end
+ return nil if dn.empty?
+ logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
+ # authenticate user
+ ldap_con = initialize_ldap_con(dn, password)
+ return nil unless ldap_con.bind
+ # return user's attributes
+ logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
+ attrs
+ rescue Net::LDAP::LdapError => text
+ raise "LdapError: " + text
+ end
+
+ # test the connection to the LDAP
+ def test_connection
+ ldap_con = initialize_ldap_con(self.account, self.account_password)
+ ldap_con.open { }
+ rescue Net::LDAP::LdapError => text
+ raise "LdapError: " + text
+ end
+
+ def auth_method_name
+ "LDAP"
+ end
+
+ private
+
+ def strip_ldap_attributes
+ [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
+ write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
+ end
+ end
+
+ def initialize_ldap_con(ldap_user, ldap_password)
+ options = { :host => self.host,
+ :port => self.port,
+ :encryption => (self.tls ? :simple_tls : nil)
+ }
+ options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
+ Net::LDAP.new options
+ end
+
+ def self.get_attr(entry, attr_name)
+ if !attr_name.blank?
+ entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Board < ActiveRecord::Base
+ belongs_to :project
+ has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
+ has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
+ belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
+ acts_as_list :scope => :project_id
+ acts_as_watchable
+
+ validates_presence_of :name, :description
+ validates_length_of :name, :maximum => 30
+ validates_length_of :description, :maximum => 255
+
+ def to_s
+ name
+ end
+
+ def reset_counters!
+ self.class.reset_counters!(id)
+ end
+
+ # Updates topics_count, messages_count and last_message_id attributes for +board_id+
+ def self.reset_counters!(board_id)
+ board_id = board_id.to_i
+ update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
+ " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
+ " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
+ ["id = ?", board_id])
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Change < ActiveRecord::Base
+ belongs_to :changeset
+
+ validates_presence_of :changeset_id, :action, :path
+
+ def relative_path
+ changeset.repository.relative_path(path)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'iconv'
+
+class Changeset < ActiveRecord::Base
+ belongs_to :repository
+ belongs_to :user
+ has_many :changes, :dependent => :delete_all
+ has_and_belongs_to_many :issues
+
+ acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
+ :description => :long_comments,
+ :datetime => :committed_on,
+ :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
+
+ acts_as_searchable :columns => 'comments',
+ :include => {:repository => :project},
+ :project_key => "#{Repository.table_name}.project_id",
+ :date_column => 'committed_on'
+
+ acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
+ :author_key => :user_id,
+ :find_options => {:include => [:user, {:repository => :project}]}
+
+ validates_presence_of :repository_id, :revision, :committed_on, :commit_date
+ validates_uniqueness_of :revision, :scope => :repository_id
+ validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
+
+ def revision=(r)
+ write_attribute :revision, (r.nil? ? nil : r.to_s)
+ end
+
+ def comments=(comment)
+ write_attribute(:comments, Changeset.normalize_comments(comment))
+ end
+
+ def committed_on=(date)
+ self.commit_date = date
+ super
+ end
+
+ def project
+ repository.project
+ end
+
+ def author
+ user || committer.to_s.split('<').first
+ end
+
+ def before_create
+ self.user = repository.find_committer_user(committer)
+ end
+
+ def after_create
+ scan_comment_for_issue_ids
+ end
+ require 'pp'
+
+ def scan_comment_for_issue_ids
+ return if comments.blank?
+ # keywords used to reference issues
+ ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
+ # keywords used to fix issues
+ fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
+ # status and optional done ratio applied
+ fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
+ done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
+
+ kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
+ return if kw_regexp.blank?
+
+ referenced_issues = []
+
+ if ref_keywords.delete('*')
+ # find any issue ID in the comments
+ target_issue_ids = []
+ comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
+ referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
+ end
+
+ comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
+ action = match[0]
+ target_issue_ids = match[1].scan(/\d+/)
+ target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
+ if fix_status && fix_keywords.include?(action.downcase)
+ # update status of issues
+ logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
+ target_issues.each do |issue|
+ # the issue may have been updated by the closure of another one (eg. duplicate)
+ issue.reload
+ # don't change the status is the issue is closed
+ next if issue.status.is_closed?
+ csettext = "r#{self.revision}"
+ if self.scmid && (! (csettext =~ /^r[0-9]+$/))
+ csettext = "commit:\"#{self.scmid}\""
+ end
+ journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
+ issue.status = fix_status
+ issue.done_ratio = done_ratio if done_ratio
+ Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
+ { :changeset => self, :issue => issue })
+ issue.save
+ end
+ end
+ referenced_issues += target_issues
+ end
+
+ self.issues = referenced_issues.uniq
+ end
+
+ def short_comments
+ @short_comments || split_comments.first
+ end
+
+ def long_comments
+ @long_comments || split_comments.last
+ end
+
+ # Returns the previous changeset
+ def previous
+ @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
+ end
+
+ # Returns the next changeset
+ def next
+ @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
+ end
+
+ # Strips and reencodes a commit log before insertion into the database
+ def self.normalize_comments(str)
+ to_utf8(str.to_s.strip)
+ end
+
+ private
+
+ def split_comments
+ comments =~ /\A(.+?)\r?\n(.*)$/m
+ @short_comments = $1 || comments
+ @long_comments = $2.to_s.strip
+ return @short_comments, @long_comments
+ end
+
+ def self.to_utf8(str)
+ return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
+ encoding = Setting.commit_logs_encoding.to_s.strip
+ unless encoding.blank? || encoding == 'UTF-8'
+ begin
+ return Iconv.conv('UTF-8', encoding, str)
+ rescue Iconv::Failure
+ # do nothing here
+ end
+ end
+ str
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Comment < ActiveRecord::Base
+ belongs_to :commented, :polymorphic => true, :counter_cache => true
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+
+ validates_presence_of :commented, :author, :comments
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class CustomField < ActiveRecord::Base
+ has_many :custom_values, :dependent => :delete_all
+ acts_as_list :scope => 'type = \'#{self.class}\''
+ serialize :possible_values
+
+ FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
+ "text" => { :name => :label_text, :order => 2 },
+ "int" => { :name => :label_integer, :order => 3 },
+ "float" => { :name => :label_float, :order => 4 },
+ "list" => { :name => :label_list, :order => 5 },
+ "date" => { :name => :label_date, :order => 6 },
+ "bool" => { :name => :label_boolean, :order => 7 }
+ }.freeze
+
+ validates_presence_of :name, :field_format
+ validates_uniqueness_of :name, :scope => :type
+ validates_length_of :name, :maximum => 30
+ validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
+ validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
+
+ def initialize(attributes = nil)
+ super
+ self.possible_values ||= []
+ end
+
+ def before_validation
+ # make sure these fields are not searchable
+ self.searchable = false if %w(int float date bool).include?(field_format)
+ true
+ end
+
+ def validate
+ if self.field_format == "list"
+ errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
+ errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
+ end
+
+ # validate default value
+ v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
+ v.custom_field.is_required = false
+ errors.add(:default_value, :invalid) unless v.valid?
+ end
+
+ # Makes possible_values accept a multiline string
+ def possible_values=(arg)
+ if arg.is_a?(Array)
+ write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
+ else
+ self.possible_values = arg.to_s.split(/[\n\r]+/)
+ end
+ end
+
+ # Returns a ORDER BY clause that can used to sort customized
+ # objects by their value of the custom field.
+ # Returns false, if the custom field can not be used for sorting.
+ def order_statement
+ case field_format
+ when 'string', 'text', 'list', 'date', 'bool'
+ # COALESCE is here to make sure that blank and NULL values are sorted equally
+ "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
+ " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
+ " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
+ " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
+ when 'int', 'float'
+ # Make the database cast values into numeric
+ # Postgresql will raise an error if a value can not be casted!
+ # CustomValue validations should ensure that it doesn't occur
+ "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
+ " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
+ " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
+ " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
+ else
+ nil
+ end
+ end
+
+ def <=>(field)
+ position <=> field.position
+ end
+
+ def self.customized_class
+ self.name =~ /^(.+)CustomField$/
+ begin; $1.constantize; rescue nil; end
+ end
+
+ # to move in project_custom_field
+ def self.for_all
+ find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
+ end
+
+ def type_name
+ nil
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class CustomValue < ActiveRecord::Base
+ belongs_to :custom_field
+ belongs_to :customized, :polymorphic => true
+
+ def after_initialize
+ if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
+ self.value ||= custom_field.default_value
+ end
+ end
+
+ # Returns true if the boolean custom value is true
+ def true?
+ self.value == '1'
+ end
+
+ def editable?
+ custom_field.editable?
+ end
+
+ def required?
+ custom_field.is_required?
+ end
+
+ def to_s
+ value.to_s
+ end
+
+protected
+ def validate
+ if value.blank?
+ errors.add(:value, :blank) if custom_field.is_required? and value.blank?
+ else
+ errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
+ errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length
+ errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length
+
+ # Format specific validations
+ case custom_field.field_format
+ when 'int'
+ errors.add(:value, :not_a_number) unless value =~ /^[+-]?\d+$/
+ when 'float'
+ begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end
+ when 'date'
+ errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
+ when 'list'
+ errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Document < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
+ acts_as_attachable :delete_permission => :manage_documents
+
+ acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
+ acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
+ :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
+ :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
+ acts_as_activity_provider :find_options => {:include => :project}
+
+ validates_presence_of :project, :title, :category
+ validates_length_of :title, :maximum => 60
+
+ def after_initialize
+ if new_record?
+ self.category ||= DocumentCategory.default
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class DocumentCategory < Enumeration
+ has_many :documents, :foreign_key => 'category_id'
+
+ OptionName = :enumeration_doc_categories
+ # Backwards compatiblity. Can be removed post-0.9
+ OptName = 'DCAT'
+
+ def option_name
+ OptionName
+ end
+
+ def objects_count
+ documents.count
+ end
+
+ def transfer_relations(to)
+ documents.update_all("category_id = #{to.id}")
+ end
+end
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+class DocumentCategoryCustomField < CustomField\r
+ def type_name\r
+ :enumeration_doc_categories\r
+ end\r
+end\r
+\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 DocumentObserver < ActiveRecord::Observer
+ def after_create(document)
+ Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class EnabledModule < ActiveRecord::Base
+ belongs_to :project
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => :project_id
+
+ after_create :module_enabled
+
+ private
+
+ # after_create callback used to do things when a module is enabled
+ def module_enabled
+ case name
+ when 'wiki'
+ # Create a wiki with a default start page
+ if project && project.wiki.nil?
+ Wiki.create(:project => project, :start_page => 'Wiki')
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Enumeration < ActiveRecord::Base
+ default_scope :order => "#{Enumeration.table_name}.position ASC"
+
+ belongs_to :project
+
+ acts_as_list :scope => 'type = \'#{type}\''
+ acts_as_customizable
+ acts_as_tree :order => 'position ASC'
+
+ before_destroy :check_integrity
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => [:type, :project_id]
+ validates_length_of :name, :maximum => 30
+
+ # Backwards compatiblity named_scopes.
+ # Can be removed post-0.9
+ named_scope :priorities, :conditions => { :type => "IssuePriority" }, :order => 'position' do
+ ActiveSupport::Deprecation.warn("Enumeration#priorities is deprecated, use the IssuePriority class. (#{Redmine::Info.issue(3007)})")
+ def default
+ find(:first, :conditions => { :is_default => true })
+ end
+ end
+
+ named_scope :document_categories, :conditions => { :type => "DocumentCategory" }, :order => 'position' do
+ ActiveSupport::Deprecation.warn("Enumeration#document_categories is deprecated, use the DocumentCategories class. (#{Redmine::Info.issue(3007)})")
+ def default
+ find(:first, :conditions => { :is_default => true })
+ end
+ end
+
+ named_scope :activities, :conditions => { :type => "TimeEntryActivity" }, :order => 'position' do
+ ActiveSupport::Deprecation.warn("Enumeration#activities is deprecated, use the TimeEntryActivity class. (#{Redmine::Info.issue(3007)})")
+ def default
+ find(:first, :conditions => { :is_default => true })
+ end
+ end
+
+ named_scope :values, lambda {|type| { :conditions => { :type => type }, :order => 'position' } } do
+ def default
+ find(:first, :conditions => { :is_default => true })
+ end
+ end
+ # End backwards compatiblity named_scopes
+
+ named_scope :shared, :conditions => { :project_id => nil }
+ named_scope :active, :conditions => { :active => true }
+
+ def self.default
+ # Creates a fake default scope so Enumeration.default will check
+ # it's type. STI subclasses will automatically add their own
+ # types to the finder.
+ if self.descends_from_active_record?
+ find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
+ else
+ # STI classes are
+ find(:first, :conditions => { :is_default => true })
+ end
+ end
+
+ # Overloaded on concrete classes
+ def option_name
+ nil
+ end
+
+ # Backwards compatiblity. Can be removed post-0.9
+ def opt
+ ActiveSupport::Deprecation.warn("Enumeration#opt is deprecated, use the STI classes now. (#{Redmine::Info.issue(3007)})")
+ return OptName
+ end
+
+ def before_save
+ if is_default? && is_default_changed?
+ Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
+ end
+ end
+
+ # Overloaded on concrete classes
+ def objects_count
+ 0
+ end
+
+ def in_use?
+ self.objects_count != 0
+ end
+
+ # Is this enumeration overiding a system level enumeration?
+ def is_override?
+ !self.parent.nil?
+ end
+
+ alias :destroy_without_reassign :destroy
+
+ # Destroy the enumeration
+ # If a enumeration is specified, objects are reassigned
+ def destroy(reassign_to = nil)
+ if reassign_to && reassign_to.is_a?(Enumeration)
+ self.transfer_relations(reassign_to)
+ end
+ destroy_without_reassign
+ end
+
+ def <=>(enumeration)
+ position <=> enumeration.position
+ end
+
+ def to_s; name end
+
+ # Returns the Subclasses of Enumeration. Each Subclass needs to be
+ # required in development mode.
+ #
+ # Note: subclasses is protected in ActiveRecord
+ def self.get_subclasses
+ @@subclasses[Enumeration]
+ end
+
+ # Does the +new+ Hash override the previous Enumeration?
+ def self.overridding_change?(new, previous)
+ if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
+ return false
+ else
+ return true
+ end
+ end
+
+ # Does the +new+ Hash have the same custom values as the previous Enumeration?
+ def self.same_custom_values?(new, previous)
+ previous.custom_field_values.each do |custom_value|
+ if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
+ return false
+ end
+ end
+
+ return true
+ end
+
+ # Are the new and previous fields equal?
+ def self.same_active_state?(new, previous)
+ new = (new == "1" ? true : false)
+ return new == previous
+ end
+
+private
+ def check_integrity
+ raise "Can't delete enumeration" if self.in_use?
+ end
+
+end
+
+# Force load the subclasses in development mode
+require_dependency 'time_entry_activity'
+require_dependency 'document_category'
+require_dependency 'issue_priority'
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Group < Principal
+ has_and_belongs_to_many :users, :after_add => :user_added,
+ :after_remove => :user_removed
+
+ acts_as_customizable
+
+ validates_presence_of :lastname
+ validates_uniqueness_of :lastname, :case_sensitive => false
+ validates_length_of :lastname, :maximum => 30
+
+ def to_s
+ lastname.to_s
+ end
+
+ def user_added(user)
+ members.each do |member|
+ user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
+ member.member_roles.each do |member_role|
+ user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
+ end
+ user_member.save!
+ end
+ end
+
+ def user_removed(user)
+ members.each do |member|
+ MemberRole.find(:all, :include => :member,
+ :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class GroupCustomField < CustomField
+ def type_name
+ :label_group_plural
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Issue < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :tracker
+ belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+ belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
+ belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
+ belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
+ belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
+
+ has_many :journals, :as => :journalized, :dependent => :destroy
+ has_many :time_entries, :dependent => :delete_all
+ has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
+
+ has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
+ has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
+
+ acts_as_attachable :after_remove => :attachment_removed
+ acts_as_customizable
+ acts_as_watchable
+ acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
+ :include => [:project, :journals],
+ # sort by id so that limited eager loading doesn't break with postgresql
+ :order_column => "#{table_name}.id"
+ acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
+ :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
+ :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
+
+ acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
+ :author_key => :author_id
+
+ validates_presence_of :subject, :priority, :project, :tracker, :author, :status
+ validates_length_of :subject, :maximum => 255
+ validates_inclusion_of :done_ratio, :in => 0..100
+ validates_numericality_of :estimated_hours, :allow_nil => true
+
+ named_scope :visible, lambda {|*args| { :include => :project,
+ :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
+
+ named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
+
+ after_save :create_journal
+
+ # Returns true if usr or current user is allowed to view the issue
+ def visible?(usr=nil)
+ (usr || User.current).allowed_to?(:view_issues, self.project)
+ end
+
+ def after_initialize
+ if new_record?
+ # set default values for new records only
+ self.status ||= IssueStatus.default
+ self.priority ||= IssuePriority.default
+ end
+ end
+
+ # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
+ def available_custom_fields
+ (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
+ end
+
+ def copy_from(arg)
+ issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
+ self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
+ self.custom_values = issue.custom_values.collect {|v| v.clone}
+ self.status = issue.status
+ self
+ end
+
+ # Moves/copies an issue to a new project and tracker
+ # Returns the moved/copied issue on success, false on failure
+ def move_to(new_project, new_tracker = nil, options = {})
+ options ||= {}
+ issue = options[:copy] ? self.clone : self
+ transaction do
+ if new_project && issue.project_id != new_project.id
+ # delete issue relations
+ unless Setting.cross_project_issue_relations?
+ issue.relations_from.clear
+ issue.relations_to.clear
+ end
+ # issue is moved to another project
+ # reassign to the category with same name if any
+ new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
+ issue.category = new_category
+ issue.fixed_version = nil
+ issue.project = new_project
+ end
+ if new_tracker
+ issue.tracker = new_tracker
+ end
+ if options[:copy]
+ issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
+ issue.status = self.status
+ end
+ if issue.save
+ unless options[:copy]
+ # Manually update project_id on related time entries
+ TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
+ end
+ else
+ Issue.connection.rollback_db_transaction
+ return false
+ end
+ end
+ return issue
+ end
+
+ def priority_id=(pid)
+ self.priority = nil
+ write_attribute(:priority_id, pid)
+ end
+
+ def estimated_hours=(h)
+ write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
+ end
+
+ def validate
+ if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
+ errors.add :due_date, :not_a_date
+ end
+
+ if self.due_date and self.start_date and self.due_date < self.start_date
+ errors.add :due_date, :greater_than_start_date
+ end
+
+ if start_date && soonest_start && start_date < soonest_start
+ errors.add :start_date, :invalid
+ end
+
+ if fixed_version
+ if !assignable_versions.include?(fixed_version)
+ errors.add :fixed_version_id, :inclusion
+ elsif reopened? && fixed_version.closed?
+ errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
+ end
+ end
+ end
+
+ def validate_on_create
+ errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
+ end
+
+ def before_create
+ # default assignment based on category
+ if assigned_to.nil? && category && category.assigned_to
+ self.assigned_to = category.assigned_to
+ end
+ end
+
+ def after_save
+ # Reload is needed in order to get the right status
+ reload
+
+ # Update start/due dates of following issues
+ relations_from.each(&:set_issue_to_dates)
+
+ # Close duplicates if the issue was closed
+ if @issue_before_change && !@issue_before_change.closed? && self.closed?
+ duplicates.each do |duplicate|
+ # Reload is need in case the duplicate was updated by a previous duplicate
+ duplicate.reload
+ # Don't re-close it if it's already closed
+ next if duplicate.closed?
+ # Same user and notes
+ duplicate.init_journal(@current_journal.user, @current_journal.notes)
+ duplicate.update_attribute :status, self.status
+ end
+ end
+ end
+
+ def init_journal(user, notes = "")
+ @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
+ @issue_before_change = self.clone
+ @issue_before_change.status = self.status
+ @custom_values_before_change = {}
+ self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
+ # Make sure updated_on is updated when adding a note.
+ updated_on_will_change!
+ @current_journal
+ end
+
+ # Return true if the issue is closed, otherwise false
+ def closed?
+ self.status.is_closed?
+ end
+
+ # Return true if the issue is being reopened
+ def reopened?
+ if !new_record? && status_id_changed?
+ status_was = IssueStatus.find_by_id(status_id_was)
+ status_new = IssueStatus.find_by_id(status_id)
+ if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
+ return true
+ end
+ end
+ false
+ end
+
+ # Returns true if the issue is overdue
+ def overdue?
+ !due_date.nil? && (due_date < Date.today) && !status.is_closed?
+ end
+
+ # Users the issue can be assigned to
+ def assignable_users
+ project.assignable_users
+ end
+
+ # Versions that the issue can be assigned to
+ def assignable_versions
+ @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
+ end
+
+ # Returns true if this issue is blocked by another issue that is still open
+ def blocked?
+ !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
+ end
+
+ # Returns an array of status that user is able to apply
+ def new_statuses_allowed_to(user)
+ statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
+ statuses << status unless statuses.empty?
+ statuses = statuses.uniq.sort
+ blocked? ? statuses.reject {|s| s.is_closed?} : statuses
+ end
+
+ # Returns the mail adresses of users that should be notified for the issue
+ def recipients
+ recipients = project.recipients
+ # Author and assignee are always notified unless they have been locked
+ recipients << author.mail if author && author.active?
+ recipients << assigned_to.mail if assigned_to && assigned_to.active?
+ recipients.compact.uniq
+ end
+
+ # Returns the total number of hours spent on this issue.
+ #
+ # Example:
+ # spent_hours => 0
+ # spent_hours => 50
+ def spent_hours
+ @spent_hours ||= time_entries.sum(:hours) || 0
+ end
+
+ def relations
+ (relations_from + relations_to).sort
+ end
+
+ def all_dependent_issues
+ dependencies = []
+ relations_from.each do |relation|
+ dependencies << relation.issue_to
+ dependencies += relation.issue_to.all_dependent_issues
+ end
+ dependencies
+ end
+
+ # Returns an array of issues that duplicate this one
+ def duplicates
+ relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
+ end
+
+ # Returns the due date or the target due date if any
+ # Used on gantt chart
+ def due_before
+ due_date || (fixed_version ? fixed_version.effective_date : nil)
+ end
+
+ # Returns the time scheduled for this issue.
+ #
+ # Example:
+ # Start Date: 2/26/09, End Date: 3/04/09
+ # duration => 6
+ def duration
+ (start_date && due_date) ? due_date - start_date : 0
+ end
+
+ def soonest_start
+ @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
+ end
+
+ def to_s
+ "#{tracker} ##{id}: #{subject}"
+ end
+
+ # Returns a string of css classes that apply to the issue
+ def css_classes
+ s = "issue status-#{status.position} priority-#{priority.position}"
+ s << ' closed' if closed?
+ s << ' overdue' if overdue?
+ s << ' created-by-me' if User.current.logged? && author_id == User.current.id
+ s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
+ s
+ end
+
+ private
+
+ # Callback on attachment deletion
+ def attachment_removed(obj)
+ journal = init_journal(User.current)
+ journal.details << JournalDetail.new(:property => 'attachment',
+ :prop_key => obj.id,
+ :old_value => obj.filename)
+ journal.save
+ end
+
+ # Saves the changes in a Journal
+ # Called after_save
+ def create_journal
+ if @current_journal
+ # attributes changes
+ (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
+ @current_journal.details << JournalDetail.new(:property => 'attr',
+ :prop_key => c,
+ :old_value => @issue_before_change.send(c),
+ :value => send(c)) unless send(c)==@issue_before_change.send(c)
+ }
+ # custom fields changes
+ custom_values.each {|c|
+ next if (@custom_values_before_change[c.custom_field_id]==c.value ||
+ (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
+ @current_journal.details << JournalDetail.new(:property => 'cf',
+ :prop_key => c.custom_field_id,
+ :old_value => @custom_values_before_change[c.custom_field_id],
+ :value => c.value)
+ }
+ @current_journal.save
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueCategory < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
+ has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => [:project_id]
+ validates_length_of :name, :maximum => 30
+
+ alias :destroy_without_reassign :destroy
+
+ # Destroy the category
+ # If a category is specified, issues are reassigned to this category
+ def destroy(reassign_to = nil)
+ if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
+ Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
+ end
+ destroy_without_reassign
+ end
+
+ def <=>(category)
+ name <=> category.name
+ end
+
+ def to_s; name end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueCustomField < CustomField
+ has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
+ has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
+ has_many :issues, :through => :issue_custom_values
+
+ def type_name
+ :label_issue_plural
+ end
+end
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueObserver < ActiveRecord::Observer
+ def after_create(issue)
+ Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssuePriority < Enumeration
+ has_many :issues, :foreign_key => 'priority_id'
+
+ OptionName = :enumeration_issue_priorities
+ # Backwards compatiblity. Can be removed post-0.9
+ OptName = 'IPRI'
+
+ def option_name
+ OptionName
+ end
+
+ def objects_count
+ issues.count
+ end
+
+ def transfer_relations(to)
+ issues.update_all("priority_id = #{to.id}")
+ end
+end
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+class IssuePriorityCustomField < CustomField\r
+ def type_name\r
+ :enumeration_issue_priorities\r
+ end\r
+end\r
+\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 IssueRelation < ActiveRecord::Base
+ belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
+ belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
+
+ TYPE_RELATES = "relates"
+ TYPE_DUPLICATES = "duplicates"
+ TYPE_BLOCKS = "blocks"
+ TYPE_PRECEDES = "precedes"
+
+ TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
+ TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
+ TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
+ TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
+ }.freeze
+
+ validates_presence_of :issue_from, :issue_to, :relation_type
+ validates_inclusion_of :relation_type, :in => TYPES.keys
+ validates_numericality_of :delay, :allow_nil => true
+ validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
+
+ attr_protected :issue_from_id, :issue_to_id
+
+ def validate
+ if issue_from && issue_to
+ errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
+ errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
+ errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
+ end
+ end
+
+ def other_issue(issue)
+ (self.issue_from_id == issue.id) ? issue_to : issue_from
+ end
+
+ def label_for(issue)
+ TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
+ end
+
+ def before_save
+ if TYPE_PRECEDES == relation_type
+ self.delay ||= 0
+ else
+ self.delay = nil
+ end
+ set_issue_to_dates
+ end
+
+ def set_issue_to_dates
+ soonest_start = self.successor_soonest_start
+ if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
+ issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
+ issue_to.save
+ end
+ end
+
+ def successor_soonest_start
+ return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
+ (issue_from.due_date || issue_from.start_date) + 1 + delay
+ end
+
+ def <=>(relation)
+ TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueStatus < ActiveRecord::Base
+ before_destroy :check_integrity
+ has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
+ acts_as_list
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+ validates_length_of :name, :maximum => 30
+ validates_format_of :name, :with => /^[\w\s\'\-]*$/i
+
+ def after_save
+ IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
+ end
+
+ # Returns the default status for new issues
+ def self.default
+ find(:first, :conditions =>["is_default=?", true])
+ end
+
+ # Returns an array of all statuses the given role can switch to
+ # Uses association cache when called more than one time
+ def new_statuses_allowed_to(roles, tracker)
+ if roles && tracker
+ role_ids = roles.collect(&:id)
+ new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
+ else
+ []
+ end
+ end
+
+ # Same thing as above but uses a database query
+ # More efficient than the previous method if called just once
+ def find_new_statuses_allowed_to(roles, tracker)
+ if roles && tracker
+ workflows.find(:all,
+ :include => :new_status,
+ :conditions => { :role_id => roles.collect(&:id),
+ :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
+ else
+ []
+ end
+ end
+
+ def new_status_allowed_to?(status, roles, tracker)
+ if status && roles && tracker
+ !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
+ else
+ false
+ end
+ end
+
+ def <=>(status)
+ position <=> status.position
+ end
+
+ def to_s; name end
+
+private
+ def check_integrity
+ raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Journal < ActiveRecord::Base
+ belongs_to :journalized, :polymorphic => true
+ # added as a quick fix to allow eager loading of the polymorphic association
+ # since always associated to an issue, for now
+ belongs_to :issue, :foreign_key => :journalized_id
+
+ belongs_to :user
+ has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
+ attr_accessor :indice
+
+ acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
+ :description => :notes,
+ :author => :user,
+ :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
+ :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
+
+ acts_as_activity_provider :type => 'issues',
+ :permission => :view_issues,
+ :author_key => :user_id,
+ :find_options => {:include => [{:issue => :project}, :details, :user],
+ :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
+ " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
+
+ def save(*args)
+ # Do not save an empty journal
+ (details.empty? && notes.blank?) ? false : super
+ end
+
+ # Returns the new status if the journal contains a status change, otherwise nil
+ def new_status
+ c = details.detect {|detail| detail.prop_key == 'status_id'}
+ (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
+ end
+
+ def new_value_for(prop)
+ c = details.detect {|detail| detail.prop_key == prop}
+ c ? c.value : nil
+ end
+
+ def editable_by?(usr)
+ usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
+ end
+
+ def project
+ journalized.respond_to?(:project) ? journalized.project : nil
+ end
+
+ def attachments
+ journalized.respond_to?(:attachments) ? journalized.attachments : nil
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class JournalDetail < ActiveRecord::Base
+ belongs_to :journal
+
+ def before_save
+ self.value = value[0..254] if value && value.is_a?(String)
+ self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 JournalObserver < ActiveRecord::Observer
+ def after_create(journal)
+ Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MailHandler < ActionMailer::Base
+ include ActionView::Helpers::SanitizeHelper
+
+ class UnauthorizedAction < StandardError; end
+ class MissingInformation < StandardError; end
+
+ attr_reader :email, :user
+
+ def self.receive(email, options={})
+ @@handler_options = options.dup
+
+ @@handler_options[:issue] ||= {}
+
+ @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
+ @@handler_options[:allow_override] ||= []
+ # Project needs to be overridable if not specified
+ @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
+ # Status overridable by default
+ @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
+ super email
+ end
+
+ # Processes incoming emails
+ # Returns the created object (eg. an issue, a message) or false
+ def receive(email)
+ @email = email
+ sender_email = email.from.to_a.first.to_s.strip
+ # Ignore emails received from the application emission address to avoid hell cycles
+ if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
+ logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
+ return false
+ end
+ @user = User.find_by_mail(sender_email)
+ if @user && !@user.active?
+ logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
+ return false
+ end
+ if @user.nil?
+ # Email was submitted by an unknown user
+ case @@handler_options[:unknown_user]
+ when 'accept'
+ @user = User.anonymous
+ when 'create'
+ @user = MailHandler.create_user_from_email(email)
+ if @user
+ logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
+ Mailer.deliver_account_information(@user, @user.password)
+ else
+ logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
+ return false
+ end
+ else
+ # Default behaviour, emails from unknown users are ignored
+ logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
+ return false
+ end
+ end
+ User.current = @user
+ dispatch
+ end
+
+ private
+
+ MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
+ ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
+ MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
+
+ def dispatch
+ headers = [email.in_reply_to, email.references].flatten.compact
+ if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
+ klass, object_id = $1, $2.to_i
+ method_name = "receive_#{klass}_reply"
+ if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
+ send method_name, object_id
+ else
+ # ignoring it
+ end
+ elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
+ receive_issue_reply(m[1].to_i)
+ elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
+ receive_message_reply(m[1].to_i)
+ else
+ receive_issue
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ # TODO: send a email to the user
+ logger.error e.message if logger
+ false
+ rescue MissingInformation => e
+ logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
+ false
+ rescue UnauthorizedAction => e
+ logger.error "MailHandler: unauthorized attempt from #{user}" if logger
+ false
+ end
+
+ # Creates a new issue
+ def receive_issue
+ project = target_project
+ tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
+ category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
+ priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
+ status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
+
+ # check permission
+ raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
+ issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
+ # check workflow
+ if status && issue.new_statuses_allowed_to(user).include?(status)
+ issue.status = status
+ end
+ issue.subject = email.subject.chomp
+ issue.subject = issue.subject.toutf8 if issue.subject.respond_to?(:toutf8)
+ if issue.subject.blank?
+ issue.subject = '(no subject)'
+ end
+ issue.description = plain_text_body
+ # custom fields
+ issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
+ if value = get_keyword(c.name, :override => true)
+ h[c.id] = value
+ end
+ h
+ end
+ # add To and Cc as watchers before saving so the watchers can reply to Redmine
+ add_watchers(issue)
+ issue.save!
+ add_attachments(issue)
+ logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
+ issue
+ end
+
+ def target_project
+ # TODO: other ways to specify project:
+ # * parse the email To field
+ # * specific project (eg. Setting.mail_handler_target_project)
+ target = Project.find_by_identifier(get_keyword(:project))
+ raise MissingInformation.new('Unable to determine target project') if target.nil?
+ target
+ end
+
+ # Adds a note to an existing issue
+ def receive_issue_reply(issue_id)
+ status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
+
+ issue = Issue.find_by_id(issue_id)
+ return unless issue
+ # check permission
+ raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
+ raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
+
+ # add the note
+ journal = issue.init_journal(user, plain_text_body)
+ add_attachments(issue)
+ # check workflow
+ if status && issue.new_statuses_allowed_to(user).include?(status)
+ issue.status = status
+ end
+ issue.save!
+ logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
+ journal
+ end
+
+ # Reply will be added to the issue
+ def receive_journal_reply(journal_id)
+ journal = Journal.find_by_id(journal_id)
+ if journal && journal.journalized_type == 'Issue'
+ receive_issue_reply(journal.journalized_id)
+ end
+ end
+
+ # Receives a reply to a forum message
+ def receive_message_reply(message_id)
+ message = Message.find_by_id(message_id)
+ if message
+ message = message.root
+ if user.allowed_to?(:add_messages, message.project) && !message.locked?
+ reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
+ :content => plain_text_body)
+ reply.author = user
+ reply.board = message.board
+ message.children << reply
+ add_attachments(reply)
+ reply
+ else
+ raise UnauthorizedAction
+ end
+ end
+ end
+
+ def add_attachments(obj)
+ if email.has_attachments?
+ email.attachments.each do |attachment|
+ Attachment.create(:container => obj,
+ :file => attachment,
+ :author => user,
+ :content_type => attachment.content_type)
+ end
+ end
+ end
+
+ # Adds To and Cc as watchers of the given object if the sender has the
+ # appropriate permission
+ def add_watchers(obj)
+ if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
+ addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
+ unless addresses.empty?
+ watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
+ watchers.each {|w| obj.add_watcher(w)}
+ end
+ end
+ end
+
+ def get_keyword(attr, options={})
+ @keywords ||= {}
+ if @keywords.has_key?(attr)
+ @keywords[attr]
+ else
+ @keywords[attr] = begin
+ if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
+ $1.strip
+ elsif !@@handler_options[:issue][attr].blank?
+ @@handler_options[:issue][attr]
+ end
+ end
+ end
+ end
+
+ # Returns the text/plain part of the email
+ # If not found (eg. HTML-only email), returns the body with tags removed
+ def plain_text_body
+ return @plain_text_body unless @plain_text_body.nil?
+ parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
+ if parts.empty?
+ parts << @email
+ end
+ plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
+ if plain_text_part.nil?
+ # no text/plain part found, assuming html-only email
+ # strip html tags and remove doctype directive
+ @plain_text_body = strip_tags(@email.body.to_s)
+ @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
+ else
+ @plain_text_body = plain_text_part.body.to_s
+ end
+ @plain_text_body.strip!
+ @plain_text_body
+ end
+
+
+ def self.full_sanitizer
+ @full_sanitizer ||= HTML::FullSanitizer.new
+ end
+
+ # Creates a user account for the +email+ sender
+ def self.create_user_from_email(email)
+ addr = email.from_addrs.to_a.first
+ if addr && !addr.spec.blank?
+ user = User.new
+ user.mail = addr.spec
+
+ names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
+ user.firstname = names.shift
+ user.lastname = names.join(' ')
+ user.lastname = '-' if user.lastname.blank?
+
+ user.login = user.mail
+ user.password = ActiveSupport::SecureRandom.hex(5)
+ user.language = Setting.default_language
+ user.save ? user : nil
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Mailer < ActionMailer::Base
+ layout 'mailer'
+ helper :application
+ helper :issues
+ helper :custom_fields
+
+ include ActionController::UrlWriter
+ include Redmine::I18n
+
+ def self.default_url_options
+ h = Setting.host_name
+ h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
+ { :host => h, :protocol => Setting.protocol }
+ end
+
+ # Builds a tmail object used to email recipients of the added issue.
+ #
+ # Example:
+ # issue_add(issue) => tmail object
+ # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
+ def issue_add(issue)
+ redmine_headers 'Project' => issue.project.identifier,
+ 'Issue-Id' => issue.id,
+ 'Issue-Author' => issue.author.login
+ redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
+ message_id issue
+ recipients issue.recipients
+ cc(issue.watcher_recipients - @recipients)
+ subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
+ body :issue => issue,
+ :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
+ render_multipart('issue_add', body)
+ end
+
+ # Builds a tmail object used to email recipients of the edited issue.
+ #
+ # Example:
+ # issue_edit(journal) => tmail object
+ # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
+ def issue_edit(journal)
+ issue = journal.journalized.reload
+ redmine_headers 'Project' => issue.project.identifier,
+ 'Issue-Id' => issue.id,
+ 'Issue-Author' => issue.author.login
+ redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
+ message_id journal
+ references issue
+ @author = journal.user
+ recipients issue.recipients
+ # Watchers in cc
+ cc(issue.watcher_recipients - @recipients)
+ s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
+ s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
+ s << issue.subject
+ subject s
+ body :issue => issue,
+ :journal => journal,
+ :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
+
+ render_multipart('issue_edit', body)
+ end
+
+ def reminder(user, issues, days)
+ set_language_if_valid user.language
+ recipients user.mail
+ subject l(:mail_subject_reminder, issues.size)
+ body :issues => issues,
+ :days => days,
+ :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
+ render_multipart('reminder', body)
+ end
+
+ # Builds a tmail object used to email users belonging to the added document's project.
+ #
+ # Example:
+ # document_added(document) => tmail object
+ # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
+ def document_added(document)
+ redmine_headers 'Project' => document.project.identifier
+ recipients document.project.recipients
+ subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
+ body :document => document,
+ :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
+ render_multipart('document_added', body)
+ end
+
+ # Builds a tmail object used to email recipients of a project when an attachements are added.
+ #
+ # Example:
+ # attachments_added(attachments) => tmail object
+ # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
+ def attachments_added(attachments)
+ container = attachments.first.container
+ added_to = ''
+ added_to_url = ''
+ case container.class.name
+ when 'Project'
+ added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
+ added_to = "#{l(:label_project)}: #{container}"
+ when 'Version'
+ added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
+ added_to = "#{l(:label_version)}: #{container.name}"
+ when 'Document'
+ added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
+ added_to = "#{l(:label_document)}: #{container.title}"
+ end
+ redmine_headers 'Project' => container.project.identifier
+ recipients container.project.recipients
+ subject "[#{container.project.name}] #{l(:label_attachment_new)}"
+ body :attachments => attachments,
+ :added_to => added_to,
+ :added_to_url => added_to_url
+ render_multipart('attachments_added', body)
+ end
+
+ # Builds a tmail object used to email recipients of a news' project when a news item is added.
+ #
+ # Example:
+ # news_added(news) => tmail object
+ # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
+ def news_added(news)
+ redmine_headers 'Project' => news.project.identifier
+ message_id news
+ recipients news.project.recipients
+ subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
+ body :news => news,
+ :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
+ render_multipart('news_added', body)
+ end
+
+ # Builds a tmail object used to email the specified recipients of the specified message that was posted.
+ #
+ # Example:
+ # message_posted(message, recipients) => tmail object
+ # Mailer.deliver_message_posted(message, recipients) => sends an email to the recipients
+ def message_posted(message, recipients)
+ redmine_headers 'Project' => message.project.identifier,
+ 'Topic-Id' => (message.parent_id || message.id)
+ message_id message
+ references message.parent unless message.parent.nil?
+ recipients(recipients)
+ subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
+ body :message => message,
+ :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
+ render_multipart('message_posted', body)
+ end
+
+ # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
+ #
+ # Example:
+ # wiki_content_added(wiki_content) => tmail object
+ # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
+ def wiki_content_added(wiki_content)
+ redmine_headers 'Project' => wiki_content.project.identifier,
+ 'Wiki-Page-Id' => wiki_content.page.id
+ message_id wiki_content
+ recipients wiki_content.project.recipients
+ cc(wiki_content.page.wiki.watcher_recipients - recipients)
+ subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
+ body :wiki_content => wiki_content,
+ :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
+ render_multipart('wiki_content_added', body)
+ end
+
+ # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
+ #
+ # Example:
+ # wiki_content_updated(wiki_content) => tmail object
+ # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
+ def wiki_content_updated(wiki_content)
+ redmine_headers 'Project' => wiki_content.project.identifier,
+ 'Wiki-Page-Id' => wiki_content.page.id
+ message_id wiki_content
+ recipients wiki_content.project.recipients
+ cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
+ subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
+ body :wiki_content => wiki_content,
+ :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
+ :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
+ render_multipart('wiki_content_updated', body)
+ end
+
+ # Builds a tmail object used to email the specified user their account information.
+ #
+ # Example:
+ # account_information(user, password) => tmail object
+ # Mailer.deliver_account_information(user, password) => sends account information to the user
+ def account_information(user, password)
+ set_language_if_valid user.language
+ recipients user.mail
+ subject l(:mail_subject_register, Setting.app_title)
+ body :user => user,
+ :password => password,
+ :login_url => url_for(:controller => 'account', :action => 'login')
+ render_multipart('account_information', body)
+ end
+
+ # Builds a tmail object used to email all active administrators of an account activation request.
+ #
+ # Example:
+ # account_activation_request(user) => tmail object
+ # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
+ def account_activation_request(user)
+ # Send the email to all active administrators
+ recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
+ subject l(:mail_subject_account_activation_request, Setting.app_title)
+ body :user => user,
+ :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
+ render_multipart('account_activation_request', body)
+ end
+
+ # Builds a tmail object used to email the specified user that their account was activated by an administrator.
+ #
+ # Example:
+ # account_activated(user) => tmail object
+ # Mailer.deliver_account_activated(user) => sends an email to the registered user
+ def account_activated(user)
+ set_language_if_valid user.language
+ recipients user.mail
+ subject l(:mail_subject_register, Setting.app_title)
+ body :user => user,
+ :login_url => url_for(:controller => 'account', :action => 'login')
+ render_multipart('account_activated', body)
+ end
+
+ def lost_password(token)
+ set_language_if_valid(token.user.language)
+ recipients token.user.mail
+ subject l(:mail_subject_lost_password, Setting.app_title)
+ body :token => token,
+ :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
+ render_multipart('lost_password', body)
+ end
+
+ def register(token)
+ set_language_if_valid(token.user.language)
+ recipients token.user.mail
+ subject l(:mail_subject_register, Setting.app_title)
+ body :token => token,
+ :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
+ render_multipart('register', body)
+ end
+
+ def test(user)
+ set_language_if_valid(user.language)
+ recipients user.mail
+ subject 'Redmine test'
+ body :url => url_for(:controller => 'welcome')
+ render_multipart('test', body)
+ end
+
+ # Overrides default deliver! method to prevent from sending an email
+ # with no recipient, cc or bcc
+ def deliver!(mail = @mail)
+ return false if (recipients.nil? || recipients.empty?) &&
+ (cc.nil? || cc.empty?) &&
+ (bcc.nil? || bcc.empty?)
+
+ # Set Message-Id and References
+ if @message_id_object
+ mail.message_id = self.class.message_id_for(@message_id_object)
+ end
+ if @references_objects
+ mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
+ end
+ super(mail)
+ end
+
+ # Sends reminders to issue assignees
+ # Available options:
+ # * :days => how many days in the future to remind about (defaults to 7)
+ # * :tracker => id of tracker for filtering issues (defaults to all trackers)
+ # * :project => id or identifier of project to process (defaults to all projects)
+ def self.reminders(options={})
+ days = options[:days] || 7
+ project = options[:project] ? Project.find(options[:project]) : nil
+ tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
+
+ s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
+ s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
+ s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
+ s << "#{Issue.table_name}.project_id = #{project.id}" if project
+ s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
+
+ issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
+ :conditions => s.conditions
+ ).group_by(&:assigned_to)
+ issues_by_assignee.each do |assignee, issues|
+ deliver_reminder(assignee, issues, days) unless assignee.nil?
+ end
+ end
+
+ private
+ def initialize_defaults(method_name)
+ super
+ set_language_if_valid Setting.default_language
+ from Setting.mail_from
+
+ # Common headers
+ headers 'X-Mailer' => 'Redmine',
+ 'X-Redmine-Host' => Setting.host_name,
+ 'X-Redmine-Site' => Setting.app_title,
+ 'Precedence' => 'bulk',
+ 'Auto-Submitted' => 'auto-generated'
+ end
+
+ # Appends a Redmine header field (name is prepended with 'X-Redmine-')
+ def redmine_headers(h)
+ h.each { |k,v| headers["X-Redmine-#{k}"] = v }
+ end
+
+ # Overrides the create_mail method
+ def create_mail
+ # Removes the current user from the recipients and cc
+ # if he doesn't want to receive notifications about what he does
+ @author ||= User.current
+ if @author.pref[:no_self_notified]
+ recipients.delete(@author.mail) if recipients
+ cc.delete(@author.mail) if cc
+ end
+ # Blind carbon copy recipients
+ if Setting.bcc_recipients?
+ bcc([recipients, cc].flatten.compact.uniq)
+ recipients []
+ cc []
+ end
+ super
+ end
+
+ # Rails 2.3 has problems rendering implicit multipart messages with
+ # layouts so this method will wrap an multipart messages with
+ # explicit parts.
+ #
+ # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
+ # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
+
+ def render_multipart(method_name, body)
+ if Setting.plain_text_mail?
+ content_type "text/plain"
+ body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
+ else
+ content_type "multipart/alternative"
+ part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
+ part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body)
+ end
+ end
+
+ # Makes partial rendering work with Rails 1.2 (retro-compatibility)
+ def self.controller_path
+ ''
+ end unless respond_to?('controller_path')
+
+ # Returns a predictable Message-Id for the given object
+ def self.message_id_for(object)
+ # id + timestamp should reduce the odds of a collision
+ # as far as we don't send multiple emails for the same object
+ timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
+ hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
+ host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
+ host = "#{::Socket.gethostname}.redmine" if host.empty?
+ "<#{hash}@#{host}>"
+ end
+
+ private
+
+ def message_id(object)
+ @message_id_object = object
+ end
+
+ def references(object)
+ @references_objects ||= []
+ @references_objects << object
+ end
+end
+
+# Patch TMail so that message_id is not overwritten
+module TMail
+ class Mail
+ def add_message_id( fqdn = nil )
+ self.message_id ||= ::TMail::new_message_id(fqdn)
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Member < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :principal, :foreign_key => 'user_id'
+ has_many :member_roles, :dependent => :destroy
+ has_many :roles, :through => :member_roles
+ belongs_to :project
+
+ validates_presence_of :principal, :project
+ validates_uniqueness_of :user_id, :scope => :project_id
+
+ def name
+ self.user.name
+ end
+
+ alias :base_role_ids= :role_ids=
+ def role_ids=(arg)
+ ids = (arg || []).collect(&:to_i) - [0]
+ # Keep inherited roles
+ ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
+
+ new_role_ids = ids - role_ids
+ # Add new roles
+ new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
+ # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
+ member_roles.select {|mr| !ids.include?(mr.role_id)}.each(&:destroy)
+ end
+
+ def <=>(member)
+ a, b = roles.sort.first, member.roles.sort.first
+ a == b ? (principal <=> member.principal) : (a <=> b)
+ end
+
+ def deletable?
+ member_roles.detect {|mr| mr.inherited_from}.nil?
+ end
+
+ def before_destroy
+ if user
+ # remove category based auto assignments for this member
+ IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
+ end
+ end
+
+ protected
+
+ def validate
+ errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MemberRole < ActiveRecord::Base
+ belongs_to :member
+ belongs_to :role
+
+ after_destroy :remove_member_if_empty
+
+ after_create :add_role_to_group_users
+ after_destroy :remove_role_from_group_users
+
+ validates_presence_of :role
+
+ def validate
+ errors.add :role_id, :invalid if role && !role.member?
+ end
+
+ private
+
+ def remove_member_if_empty
+ if member.roles.empty?
+ member.destroy
+ end
+ end
+
+ def add_role_to_group_users
+ if member.principal.is_a?(Group)
+ member.principal.users.each do |user|
+ user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
+ user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
+ user_member.save!
+ end
+ end
+ end
+
+ def remove_role_from_group_users
+ MemberRole.find(:all, :conditions => { :inherited_from => id }).each(&:destroy)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Message < ActiveRecord::Base
+ belongs_to :board
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+ acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
+ acts_as_attachable
+ belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
+
+ acts_as_searchable :columns => ['subject', 'content'],
+ :include => {:board => :project},
+ :project_key => 'project_id',
+ :date_column => "#{table_name}.created_on"
+ acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
+ :description => :content,
+ :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
+ :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
+ {:id => o.parent_id, :anchor => "message-#{o.id}"})}
+
+ acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
+ :author_key => :author_id
+ acts_as_watchable
+
+ attr_protected :locked, :sticky
+ validates_presence_of :board, :subject, :content
+ validates_length_of :subject, :maximum => 255
+
+ after_create :add_author_as_watcher
+
+ def validate_on_create
+ # Can not reply to a locked topic
+ errors.add_to_base 'Topic is locked' if root.locked? && self != root
+ end
+
+ def after_create
+ if parent
+ parent.reload.update_attribute(:last_reply_id, self.id)
+ end
+ board.reset_counters!
+ end
+
+ def after_update
+ if board_id_changed?
+ Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
+ Board.reset_counters!(board_id_was)
+ Board.reset_counters!(board_id)
+ end
+ end
+
+ def after_destroy
+ board.reset_counters!
+ end
+
+ def sticky=(arg)
+ write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
+ end
+
+ def sticky?
+ sticky == 1
+ end
+
+ def project
+ board.project
+ end
+
+ def editable_by?(usr)
+ usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
+ end
+
+ def destroyable_by?(usr)
+ usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
+ end
+
+ private
+
+ def add_author_as_watcher
+ Watcher.create(:watchable => self.root, :user => author)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 MessageObserver < ActiveRecord::Observer
+ def after_create(message)
+ recipients = []
+ # send notification to the topic watchers
+ recipients += message.root.watcher_recipients
+ # send notification to the board watchers
+ recipients += message.board.watcher_recipients
+ # send notification to project members who want to be notified
+ recipients += message.board.project.recipients
+ recipients = recipients.compact.uniq
+ Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 News < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+ has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
+
+ validates_presence_of :title, :description
+ validates_length_of :title, :maximum => 60
+ validates_length_of :summary, :maximum => 255
+
+ acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
+ acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
+ acts_as_activity_provider :find_options => {:include => [:project, :author]},
+ :author_key => :author_id
+
+ # returns latest news for projects visible by user
+ def self.latest(user = User.current, count = 5)
+ find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 NewsObserver < ActiveRecord::Observer
+ def after_create(news)
+ Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added')
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Principal < ActiveRecord::Base
+ set_table_name 'users'
+
+ has_many :members, :foreign_key => 'user_id', :dependent => :destroy
+ has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
+ has_many :projects, :through => :memberships
+
+ # Groups and active users
+ named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
+
+ named_scope :like, lambda {|q|
+ s = "%#{q.to_s.strip.downcase}%"
+ {:conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", s, s, s],
+ :order => 'type, login, lastname, firstname'
+ }
+ }
+
+ def <=>(principal)
+ self.to_s.downcase <=> principal.to_s.downcase
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Project < ActiveRecord::Base
+ # Project statuses
+ STATUS_ACTIVE = 1
+ STATUS_ARCHIVED = 9
+
+ # Specific overidden Activities
+ has_many :time_entry_activities
+ has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
+ 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=#{User::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, :order => "#{Issue.table_name}.created_on DESC", :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, :dependent => :delete_all
+ has_many :documents, :dependent => :destroy
+ has_many :news, :dependent => :delete_all, :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, :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', '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.id}},
+ :author => nil
+
+ attr_protected :status, :enabled_module_names
+
+ validates_presence_of :name, :identifier
+ validates_uniqueness_of :name, :identifier
+ validates_associated :repository, :wiki
+ validates_length_of :name, :maximum => 30
+ validates_length_of :homepage, :maximum => 255
+ validates_length_of :identifier, :in => 1..20
+ # donwcase letters, digits, dashes but not digits only
+ validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
+ # reserved words
+ validates_exclusion_of :identifier, :in => %w( new )
+
+ before_destroy :delete_all_members
+
+ named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
+ named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
+ named_scope :all_public, { :conditions => { :is_public => true } }
+ named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
+
+ def identifier=(identifier)
+ super unless identifier_frozen?
+ end
+
+ def identifier_frozen?
+ errors[:identifier].nil? && !(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)
+ find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
+ end
+
+ # Returns a SQL :conditions string used to find all active projects for the specified user.
+ #
+ # Examples:
+ # Projects.visible_by(admin) => "projects.status = 1"
+ # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
+ def self.visible_by(user=nil)
+ user ||= User.current
+ if user && user.admin?
+ return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
+ elsif user && user.memberships.any?
+ return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
+ else
+ return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
+ end
+ end
+
+ def self.allowed_to_condition(user, permission, options={})
+ statements = []
+ base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
+ if perm = Redmine::AccessControl.permission(permission)
+ unless perm.project_module.nil?
+ # 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
+ 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?
+ # no restriction
+ else
+ statements << "1=0"
+ if user.logged?
+ if Role.non_member.allowed_to?(permission) && !options[:member]
+ statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
+ end
+ allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
+ statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
+ else
+ if Role.anonymous.allowed_to?(permission) && !options[:member]
+ # anonymous user allowed on public project
+ statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
+ end
+ end
+ end
+ statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
+ 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 to_param
+ # id is used for projects with a numeric identifier (compatibility)
+ @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
+ end
+
+ def active?
+ self.status == STATUS_ACTIVE
+ end
+
+ # Archives the project and its descendants recursively
+ def archive
+ # Archive subprojects if any
+ children.each do |subproject|
+ subproject.archive
+ end
+ update_attribute :status, STATUS_ARCHIVED
+ 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
+
+ # 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.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
+ 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))
+ # Insert the project so that target's children or root projects stay alphabetically sorted
+ sibs = (p.nil? ? self.class.roots : p.children)
+ to_be_inserted_before = sibs.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 p.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(p)
+ end
+ true
+ else
+ # Can not move to the given target
+ 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.find(:all, :include => :projects,
+ :select => "DISTINCT #{Tracker.table_name}.*",
+ :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
+ :order => "#{Tracker.table_name}.position")
+ end
+
+ # Closes open and locked project versions that are completed
+ def close_completed_versions
+ Version.transaction do
+ versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
+ if version.completed?
+ version.update_attribute(:status, 'closed')
+ end
+ end
+ end
+ end
+
+ # Returns a hash of project users grouped by role
+ def users_by_role
+ members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
+ m.roles.each do |r|
+ h[r] ||= []
+ h[r] << m.user
+ end
+ h
+ end
+ end
+
+ # Deletes all project's members
+ def delete_all_members
+ me, mr = Member.table_name, MemberRole.table_name
+ connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
+ Member.delete_all(['project_id = ?', id])
+ end
+
+ # Users issues can be assigned to
+ def assignable_users
+ members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
+ end
+
+ # Returns the mail adresses of users that should be always notified on project events
+ def recipients
+ members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
+ 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
+
+ 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
+
+ # Return true if this project is allowed to do the specified action.
+ # action can be:
+ # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
+ # * a permission Symbol (eg. :edit_project)
+ def allows_to?(action)
+ 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)
+ # remove disabled modules
+ enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
+ # add new modules
+ module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
+ else
+ enabled_modules.clear
+ end
+ end
+
+ # Returns an auto-generated project identifier based on the last identifier used
+ def self.next_identifier
+ p = Project.find(:first, :order => 'created_on DESC')
+ 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
+
+
+ # Copies +project+ and returns the new instance. This will not save
+ # the copy
+ def self.copy_from(project)
+ begin
+ project = project.is_a?(Project) ? project : Project.find(project)
+ if 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
+ return copy
+ else
+ return nil
+ end
+ rescue ActiveRecord::RecordNotFound
+ return nil
+ end
+ end
+
+ private
+
+ # Copies wiki from +project+
+ def copy_wiki(project)
+ # Check that the source project has a wiki first
+ unless project.wiki.nil?
+ self.wiki ||= Wiki.new
+ wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
+ project.wiki.pages.each do |page|
+ 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
+ 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)
+ project.issues.each do |issue|
+ new_issue = Issue.new
+ new_issue.copy_from(issue)
+ # Reassign fixed_versions by name, since names are unique per
+ # project and the versions for self are not yet saved
+ if issue.fixed_version
+ new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
+ end
+ # Reassign the category by name, since names are unique per
+ # project and the categories for self are not yet saved
+ if issue.category
+ new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
+ end
+ self.issues << new_issue
+ end
+ end
+
+ # Copies members from +project+
+ def copy_members(project)
+ project.members.each do |member|
+ new_member = Member.new
+ new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
+ new_member.role_ids = member.role_ids.dup
+ 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 = Query.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
+ 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.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.active.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.
+ find(:all,
+ :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
+ self.time_entry_activities
+ else
+ return TimeEntryActivity.shared.active.
+ find(:all,
+ :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
+ self.time_entry_activities.active
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class ProjectCustomField < CustomField
+ def type_name
+ :label_project_plural
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class QueryColumn
+ attr_accessor :name, :sortable, :groupable, :default_order, :include_options
+ include Redmine::I18n
+
+ def initialize(name, options={})
+ self.name = name
+ self.sortable = options[:sortable]
+ self.groupable = options[:groupable] || false
+ if groupable == true
+ self.groupable = name.to_s
+ end
+ self.default_order = options[:default_order]
+ self.include_options = options[:include]
+ end
+
+ def caption
+ l("field_#{name}")
+ end
+
+ # Returns true if the column is sortable, otherwise false
+ def sortable?
+ !sortable.nil?
+ end
+end
+
+class QueryCustomFieldColumn < QueryColumn
+
+ def initialize(custom_field)
+ self.name = "cf_#{custom_field.id}".to_sym
+ self.sortable = custom_field.order_statement || false
+ if %w(list date bool int).include?(custom_field.field_format)
+ self.groupable = custom_field.order_statement
+ end
+ self.groupable ||= false
+ self.include_options = :custom_values
+ @cf = custom_field
+ end
+
+ def caption
+ @cf.name
+ end
+
+ def custom_field
+ @cf
+ end
+end
+
+class Query < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :user
+ serialize :filters
+ serialize :column_names
+ serialize :sort_criteria, Array
+
+ attr_protected :project_id, :user_id
+
+ validates_presence_of :name, :on => :save
+ validates_length_of :name, :maximum => 255
+
+ @@operators = { "=" => :label_equals,
+ "!" => :label_not_equals,
+ "o" => :label_open_issues,
+ "c" => :label_closed_issues,
+ "!*" => :label_none,
+ "*" => :label_all,
+ ">=" => :label_greater_or_equal,
+ "<=" => :label_less_or_equal,
+ "<t+" => :label_in_less_than,
+ ">t+" => :label_in_more_than,
+ "t+" => :label_in,
+ "t" => :label_today,
+ "w" => :label_this_week,
+ ">t-" => :label_less_than_ago,
+ "<t-" => :label_more_than_ago,
+ "t-" => :label_ago,
+ "~" => :label_contains,
+ "!~" => :label_not_contains }
+
+ cattr_reader :operators
+
+ @@operators_by_filter_type = { :list => [ "=", "!" ],
+ :list_status => [ "o", "=", "!", "c", "*" ],
+ :list_optional => [ "=", "!", "!*", "*" ],
+ :list_subprojects => [ "*", "!*", "=" ],
+ :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
+ :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
+ :string => [ "=", "~", "!", "!~" ],
+ :text => [ "~", "!~" ],
+ :integer => [ "=", ">=", "<=", "!*", "*" ] }
+
+ cattr_reader :operators_by_filter_type
+
+ @@available_columns = [
+ QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
+ QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true, :include => :tracker),
+ QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
+ QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
+ QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
+ QueryColumn.new(:author),
+ QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true, :include => :assigned_to),
+ QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
+ QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true, :include => :category),
+ QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true, :include => :fixed_version),
+ QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
+ QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
+ QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
+ QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
+ QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
+ ]
+ cattr_reader :available_columns
+
+ def initialize(attributes = nil)
+ super attributes
+ self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
+ end
+
+ def after_initialize
+ # Store the fact that project is nil (used in #editable_by?)
+ @is_for_all = project.nil?
+ end
+
+ def validate
+ filters.each_key do |field|
+ errors.add label_for(field), :blank unless
+ # filter requires one or more values
+ (values_for(field) and !values_for(field).first.blank?) or
+ # filter doesn't require any value
+ ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
+ end if filters
+ end
+
+ def editable_by?(user)
+ return false unless user
+ # Admin can edit them all and regular users can edit their private queries
+ return true if user.admin? || (!is_public && self.user_id == user.id)
+ # Members can not edit public queries that are for all project (only admin is allowed to)
+ is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
+ end
+
+ def available_filters
+ return @available_filters if @available_filters
+
+ trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
+
+ @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
+ "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
+ "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
+ "subject" => { :type => :text, :order => 8 },
+ "created_on" => { :type => :date_past, :order => 9 },
+ "updated_on" => { :type => :date_past, :order => 10 },
+ "start_date" => { :type => :date, :order => 11 },
+ "due_date" => { :type => :date, :order => 12 },
+ "estimated_hours" => { :type => :integer, :order => 13 },
+ "done_ratio" => { :type => :integer, :order => 14 }}
+
+ user_values = []
+ user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
+ if project
+ user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
+ else
+ # members of the user's projects
+ user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
+ end
+ @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
+ @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
+
+ if User.current.logged?
+ @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
+ end
+
+ if project
+ # project specific filters
+ unless @project.issue_categories.empty?
+ @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
+ end
+ unless @project.versions.empty?
+ @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
+ end
+ unless @project.descendants.active.empty?
+ @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
+ end
+ add_custom_fields_filters(@project.all_issue_custom_fields)
+ else
+ # global filters for cross project issue list
+ add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
+ end
+ @available_filters
+ end
+
+ def add_filter(field, operator, values)
+ # values must be an array
+ return unless values and values.is_a? Array # and !values.first.empty?
+ # check if field is defined as an available filter
+ if available_filters.has_key? field
+ filter_options = available_filters[field]
+ # check if operator is allowed for that filter
+ #if @@operators_by_filter_type[filter_options[:type]].include? operator
+ # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
+ # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
+ #end
+ filters[field] = {:operator => operator, :values => values }
+ end
+ end
+
+ def add_short_filter(field, expression)
+ return unless expression
+ parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
+ add_filter field, (parms[0] || "="), [parms[1] || ""]
+ end
+
+ def has_filter?(field)
+ filters and filters[field]
+ end
+
+ def operator_for(field)
+ has_filter?(field) ? filters[field][:operator] : nil
+ end
+
+ def values_for(field)
+ has_filter?(field) ? filters[field][:values] : nil
+ end
+
+ def label_for(field)
+ label = available_filters[field][:name] if available_filters.has_key?(field)
+ label ||= field.gsub(/\_id$/, "")
+ end
+
+ def available_columns
+ return @available_columns if @available_columns
+ @available_columns = Query.available_columns
+ @available_columns += (project ?
+ project.all_issue_custom_fields :
+ IssueCustomField.find(:all)
+ ).collect {|cf| QueryCustomFieldColumn.new(cf) }
+ end
+
+ # Returns an array of columns that can be used to group the results
+ def groupable_columns
+ available_columns.select {|c| c.groupable}
+ end
+
+ def columns
+ if has_default_columns?
+ available_columns.select do |c|
+ # Adds the project column by default for cross-project lists
+ Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
+ end
+ else
+ # preserve the column_names order
+ column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
+ end
+ end
+
+ def column_names=(names)
+ names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
+ names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
+ write_attribute(:column_names, names)
+ end
+
+ def has_column?(column)
+ column_names && column_names.include?(column.name)
+ end
+
+ def has_default_columns?
+ column_names.nil? || column_names.empty?
+ end
+
+ def sort_criteria=(arg)
+ c = []
+ if arg.is_a?(Hash)
+ arg = arg.keys.sort.collect {|k| arg[k]}
+ end
+ c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
+ write_attribute(:sort_criteria, c)
+ end
+
+ def sort_criteria
+ read_attribute(:sort_criteria) || []
+ end
+
+ def sort_criteria_key(arg)
+ sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
+ end
+
+ def sort_criteria_order(arg)
+ sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
+ end
+
+ # Returns the SQL sort order that should be prepended for grouping
+ def group_by_sort_order
+ if grouped? && (column = group_by_column)
+ column.sortable.is_a?(Array) ?
+ column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
+ "#{column.sortable} #{column.default_order}"
+ end
+ end
+
+ # Returns true if the query is a grouped query
+ def grouped?
+ !group_by.blank?
+ end
+
+ def group_by_column
+ groupable_columns.detect {|c| c.name.to_s == group_by}
+ end
+
+ def group_by_statement
+ group_by_column.groupable
+ end
+
+ def include_options
+ (columns << group_by_column).collect {|column| column && column.include_options}.flatten.compact.uniq
+ end
+
+ def project_statement
+ project_clauses = []
+ if project && !@project.descendants.active.empty?
+ ids = [project.id]
+ if has_filter?("subproject_id")
+ case operator_for("subproject_id")
+ when '='
+ # include the selected subprojects
+ ids += values_for("subproject_id").each(&:to_i)
+ when '!*'
+ # main project only
+ else
+ # all subprojects
+ ids += project.descendants.collect(&:id)
+ end
+ elsif Setting.display_subprojects_issues?
+ ids += project.descendants.collect(&:id)
+ end
+ project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
+ elsif project
+ project_clauses << "#{Project.table_name}.id = %d" % project.id
+ end
+ project_clauses << Project.allowed_to_condition(User.current, :view_issues)
+ project_clauses.join(' AND ')
+ end
+
+ def statement
+ # filters clauses
+ filters_clauses = []
+ filters.each_key do |field|
+ next if field == "subproject_id"
+ v = values_for(field).clone
+ next unless v and !v.empty?
+ operator = operator_for(field)
+
+ # "me" value subsitution
+ if %w(assigned_to_id author_id watcher_id).include?(field)
+ v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
+ end
+
+ sql = ''
+ if field =~ /^cf_(\d+)$/
+ # custom field
+ db_table = CustomValue.table_name
+ db_field = 'value'
+ is_custom_filter = true
+ sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
+ sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
+ elsif field == 'watcher_id'
+ db_table = Watcher.table_name
+ db_field = 'user_id'
+ sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
+ sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
+ else
+ # regular field
+ db_table = Issue.table_name
+ db_field = field
+ sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
+ end
+ filters_clauses << sql
+
+ end if filters and valid?
+
+ (filters_clauses << project_statement).join(' AND ')
+ end
+
+ private
+
+ # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
+ def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
+ sql = ''
+ case operator
+ when "="
+ sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
+ when "!"
+ sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
+ when "!*"
+ sql = "#{db_table}.#{db_field} IS NULL"
+ sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
+ when "*"
+ sql = "#{db_table}.#{db_field} IS NOT NULL"
+ sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
+ when ">="
+ sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
+ when "<="
+ sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
+ when "o"
+ sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
+ when "c"
+ sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
+ when ">t-"
+ sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
+ when "<t-"
+ sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
+ when "t-"
+ sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
+ when ">t+"
+ sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
+ when "<t+"
+ sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
+ when "t+"
+ sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
+ when "t"
+ sql = date_range_clause(db_table, db_field, 0, 0)
+ when "w"
+ from = l(:general_first_day_of_week) == '7' ?
+ # week starts on sunday
+ ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
+ # week starts on monday (Rails default)
+ Time.now.at_beginning_of_week
+ sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
+ when "~"
+ sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
+ when "!~"
+ sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
+ end
+
+ return sql
+ end
+
+ def add_custom_fields_filters(custom_fields)
+ @available_filters ||= {}
+
+ custom_fields.select(&:is_filter?).each do |field|
+ case field.field_format
+ when "text"
+ options = { :type => :text, :order => 20 }
+ when "list"
+ options = { :type => :list_optional, :values => field.possible_values, :order => 20}
+ when "date"
+ options = { :type => :date, :order => 20 }
+ when "bool"
+ options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
+ else
+ options = { :type => :string, :order => 20 }
+ end
+ @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
+ end
+ end
+
+ # Returns a SQL clause for a date or datetime field.
+ def date_range_clause(table, field, from, to)
+ s = []
+ if from
+ s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
+ end
+ if to
+ s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
+ end
+ s.join(' AND ')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Repository < ActiveRecord::Base
+ belongs_to :project
+ has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
+ has_many :changes, :through => :changesets
+
+ # Raw SQL to delete changesets and changes in the database
+ # has_many :changesets, :dependent => :destroy is too slow for big repositories
+ before_destroy :clear_changesets
+
+ # Checks if the SCM is enabled when creating a repository
+ validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
+
+ # Removes leading and trailing whitespace
+ def url=(arg)
+ write_attribute(:url, arg ? arg.to_s.strip : nil)
+ end
+
+ # Removes leading and trailing whitespace
+ def root_url=(arg)
+ write_attribute(:root_url, arg ? arg.to_s.strip : nil)
+ end
+
+ def scm
+ @scm ||= self.scm_adapter.new url, root_url, login, password
+ update_attribute(:root_url, @scm.root_url) if root_url.blank?
+ @scm
+ end
+
+ def scm_name
+ self.class.scm_name
+ end
+
+ def supports_cat?
+ scm.supports_cat?
+ end
+
+ def supports_annotate?
+ scm.supports_annotate?
+ end
+
+ def entry(path=nil, identifier=nil)
+ scm.entry(path, identifier)
+ end
+
+ def entries(path=nil, identifier=nil)
+ scm.entries(path, identifier)
+ end
+
+ def branches
+ scm.branches
+ end
+
+ def tags
+ scm.tags
+ end
+
+ def default_branch
+ scm.default_branch
+ end
+
+ def properties(path, identifier=nil)
+ scm.properties(path, identifier)
+ end
+
+ def cat(path, identifier=nil)
+ scm.cat(path, identifier)
+ end
+
+ def diff(path, rev, rev_to)
+ scm.diff(path, rev, rev_to)
+ end
+
+ # Returns a path relative to the url of the repository
+ def relative_path(path)
+ path
+ end
+
+ # Finds and returns a revision with a number or the beginning of a hash
+ def find_changeset_by_name(name)
+ changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
+ end
+
+ def latest_changeset
+ @latest_changeset ||= changesets.find(:first)
+ end
+
+ # Returns the latest changesets for +path+
+ # Default behaviour is to search in cached changesets
+ def latest_changesets(path, rev, limit=10)
+ if path.blank?
+ changesets.find(:all, :include => :user,
+ :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
+ :limit => limit)
+ else
+ changes.find(:all, :include => {:changeset => :user},
+ :conditions => ["path = ?", path.with_leading_slash],
+ :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
+ :limit => limit).collect(&:changeset)
+ end
+ end
+
+ def scan_changesets_for_issue_ids
+ self.changesets.each(&:scan_comment_for_issue_ids)
+ end
+
+ # Returns an array of committers usernames and associated user_id
+ def committers
+ @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
+ end
+
+ # Maps committers username to a user ids
+ def committer_ids=(h)
+ if h.is_a?(Hash)
+ committers.each do |committer, user_id|
+ new_user_id = h[committer]
+ if new_user_id && (new_user_id.to_i != user_id.to_i)
+ new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
+ Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
+ end
+ end
+ @committers = nil
+ true
+ else
+ false
+ end
+ end
+
+ # Returns the Redmine User corresponding to the given +committer+
+ # It will return nil if the committer is not yet mapped and if no User
+ # with the same username or email was found
+ def find_committer_user(committer)
+ if committer
+ c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
+ if c && c.user
+ c.user
+ elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
+ username, email = $1.strip, $3
+ u = User.find_by_login(username)
+ u ||= User.find_by_mail(email) unless email.blank?
+ u
+ end
+ end
+ end
+
+ # fetch new changesets for all repositories
+ # can be called periodically by an external script
+ # eg. ruby script/runner "Repository.fetch_changesets"
+ def self.fetch_changesets
+ find(:all).each(&:fetch_changesets)
+ end
+
+ # scan changeset comments to find related and fixed issues for all repositories
+ def self.scan_changesets_for_issue_ids
+ find(:all).each(&:scan_changesets_for_issue_ids)
+ end
+
+ def self.scm_name
+ 'Abstract'
+ end
+
+ def self.available_scm
+ subclasses.collect {|klass| [klass.scm_name, klass.name]}
+ end
+
+ def self.factory(klass_name, *args)
+ klass = "Repository::#{klass_name}".constantize
+ klass.new(*args)
+ rescue
+ nil
+ end
+
+ private
+
+ def before_save
+ # Strips url and root_url
+ url.strip!
+ root_url.strip!
+ true
+ end
+
+ def clear_changesets
+ cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
+ connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
+ connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
+ connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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/bazaar_adapter'
+
+class Repository::Bazaar < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::BazaarAdapter
+ end
+
+ def self.scm_name
+ 'Bazaar'
+ end
+
+ def entries(path=nil, identifier=nil)
+ entries = scm.entries(path, identifier)
+ if entries
+ entries.each do |e|
+ next if e.lastrev.revision.blank?
+ # Set the filesize unless browsing a specific revision
+ if identifier.nil? && e.is_file?
+ full_path = File.join(root_url, e.path)
+ e.size = File.stat(full_path).size if File.file?(full_path)
+ end
+ c = Change.find(:first,
+ :include => :changeset,
+ :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
+ :order => "#{Changeset.table_name}.revision DESC")
+ if c
+ e.lastrev.identifier = c.changeset.revision
+ e.lastrev.name = c.changeset.revision
+ e.lastrev.author = c.changeset.committer
+ end
+ end
+ end
+ end
+
+ def fetch_changesets
+ scm_info = scm.info
+ if scm_info
+ # latest revision found in database
+ db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
+ # latest revision in the repository
+ scm_revision = scm_info.lastrev.identifier.to_i
+ if db_revision < scm_revision
+ logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
+ identifier_from = db_revision + 1
+ while (identifier_from <= scm_revision)
+ # loads changesets by batches of 200
+ identifier_to = [identifier_from + 199, scm_revision].min
+ revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
+ transaction do
+ revisions.reverse_each do |revision|
+ changeset = Changeset.create(:repository => self,
+ :revision => revision.identifier,
+ :committer => revision.author,
+ :committed_on => revision.time,
+ :scmid => revision.scmid,
+ :comments => revision.message)
+
+ revision.paths.each do |change|
+ Change.create(:changeset => changeset,
+ :action => change[:action],
+ :path => change[:path],
+ :revision => change[:revision])
+ end
+ end
+ end unless revisions.nil?
+ identifier_from = identifier_to + 1
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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/cvs_adapter'
+require 'digest/sha1'
+
+class Repository::Cvs < Repository
+ validates_presence_of :url, :root_url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::CvsAdapter
+ end
+
+ def self.scm_name
+ 'CVS'
+ end
+
+ def entry(path=nil, identifier=nil)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.entry(path, rev.nil? ? nil : rev.committed_on)
+ end
+
+ def entries(path=nil, identifier=nil)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
+ if entries
+ entries.each() do |entry|
+ unless entry.lastrev.nil? || entry.lastrev.identifier
+ change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
+ if change
+ entry.lastrev.identifier=change.changeset.revision
+ entry.lastrev.author=change.changeset.committer
+ entry.lastrev.revision=change.revision
+ entry.lastrev.branch=change.branch
+ end
+ end
+ end
+ end
+ entries
+ end
+
+ def cat(path, identifier=nil)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.cat(path, rev.nil? ? nil : rev.committed_on)
+ end
+
+ def diff(path, rev, rev_to)
+ #convert rev to revision. CVS can't handle changesets here
+ diff=[]
+ changeset_from=changesets.find_by_revision(rev)
+ if rev_to.to_i > 0
+ changeset_to=changesets.find_by_revision(rev_to)
+ end
+ changeset_from.changes.each() do |change_from|
+
+ revision_from=nil
+ revision_to=nil
+
+ revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
+
+ if revision_from
+ if changeset_to
+ changeset_to.changes.each() do |change_to|
+ revision_to=change_to.revision if change_to.path==change_from.path
+ end
+ end
+ unless revision_to
+ revision_to=scm.get_previous_revision(revision_from)
+ end
+ file_diff = scm.diff(change_from.path, revision_from, revision_to)
+ diff = diff + file_diff unless file_diff.nil?
+ end
+ end
+ return diff
+ end
+
+ def fetch_changesets
+ # some nifty bits to introduce a commit-id with cvs
+ # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
+ # we now take a guess using the author, the commitlog and the commit-date.
+
+ # last one is the next step to take. the commit-date is not equal for all
+ # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
+ # we use a small delta here, to merge all changes belonging to _one_ changeset
+ time_delta=10.seconds
+
+ fetch_since = latest_changeset ? latest_changeset.committed_on : nil
+ transaction do
+ tmp_rev_num = 1
+ scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
+ # only add the change to the database, if it doen't exists. the cvs log
+ # is not exclusive at all.
+ unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
+ revision
+ cs = changesets.find(:first, :conditions=>{
+ :committed_on=>revision.time-time_delta..revision.time+time_delta,
+ :committer=>revision.author,
+ :comments=>Changeset.normalize_comments(revision.message)
+ })
+
+ # create a new changeset....
+ unless cs
+ # we use a temporaray revision number here (just for inserting)
+ # later on, we calculate a continous positive number
+ latest = changesets.find(:first, :order => 'id DESC')
+ cs = Changeset.create(:repository => self,
+ :revision => "_#{tmp_rev_num}",
+ :committer => revision.author,
+ :committed_on => revision.time,
+ :comments => revision.message)
+ tmp_rev_num += 1
+ end
+
+ #convert CVS-File-States to internal Action-abbrevations
+ #default action is (M)odified
+ action="M"
+ if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
+ action="A" #add-action always at first revision (= 1.1)
+ elsif revision.paths[0][:action]=="dead"
+ action="D" #dead-state is similar to Delete
+ end
+
+ Change.create(:changeset => cs,
+ :action => action,
+ :path => scm.with_leading_slash(revision.paths[0][:path]),
+ :revision => revision.paths[0][:revision],
+ :branch => revision.paths[0][:branch]
+ )
+ end
+ end
+
+ # Renumber new changesets in chronological order
+ changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
+ changeset.update_attribute :revision, next_revision_number
+ end
+ end # transaction
+ end
+
+ private
+
+ # Returns the next revision number to assign to a CVS changeset
+ def next_revision_number
+ # Need to retrieve existing revision numbers to sort them as integers
+ @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
+ @current_revision_number += 1
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/darcs_adapter'
+
+class Repository::Darcs < Repository
+ validates_presence_of :url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::DarcsAdapter
+ end
+
+ def self.scm_name
+ 'Darcs'
+ end
+
+ def entry(path=nil, identifier=nil)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.entry(path, patch.nil? ? nil : patch.scmid)
+ end
+
+ def entries(path=nil, identifier=nil)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
+ if entries
+ entries.each do |entry|
+ # Search the DB for the entry's last change
+ changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
+ if changeset
+ entry.lastrev.identifier = changeset.revision
+ entry.lastrev.name = changeset.revision
+ entry.lastrev.time = changeset.committed_on
+ entry.lastrev.author = changeset.committer
+ end
+ end
+ end
+ entries
+ end
+
+ def cat(path, identifier=nil)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
+ scm.cat(path, patch.nil? ? nil : patch.scmid)
+ end
+
+ def diff(path, rev, rev_to)
+ patch_from = changesets.find_by_revision(rev)
+ return nil if patch_from.nil?
+ patch_to = changesets.find_by_revision(rev_to) if rev_to
+ if path.blank?
+ path = patch_from.changes.collect{|change| change.path}.join(' ')
+ end
+ patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
+ end
+
+ def fetch_changesets
+ scm_info = scm.info
+ if scm_info
+ db_last_id = latest_changeset ? latest_changeset.scmid : nil
+ next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
+ # latest revision in the repository
+ scm_revision = scm_info.lastrev.scmid
+ unless changesets.find_by_scmid(scm_revision)
+ revisions = scm.revisions('', db_last_id, nil, :with_path => true)
+ transaction do
+ revisions.reverse_each do |revision|
+ changeset = Changeset.create(:repository => self,
+ :revision => next_rev,
+ :scmid => revision.scmid,
+ :committer => revision.author,
+ :committed_on => revision.time,
+ :comments => revision.message)
+
+ revision.paths.each do |change|
+ Change.create(:changeset => changeset,
+ :action => change[:action],
+ :path => change[:path],
+ :from_path => change[:from_path],
+ :from_revision => change[:from_revision])
+ end
+ next_rev += 1
+ end if revisions
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# FileSystem adapter
+# File written by Paul Rivier, at Demotera.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/filesystem_adapter'
+
+class Repository::Filesystem < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::FilesystemAdapter
+ end
+
+ def self.scm_name
+ 'Filesystem'
+ end
+
+ def entries(path=nil, identifier=nil)
+ scm.entries(path, identifier)
+ end
+
+ def fetch_changesets
+ nil
+ end
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 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 scm_adapter
+ Redmine::Scm::Adapters::GitAdapter
+ end
+
+ def self.scm_name
+ 'Git'
+ end
+
+ def branches
+ scm.branches
+ end
+
+ def tags
+ scm.tags
+ end
+
+ # With SCM's that have a sequential commit numbering, redmine is able to be
+ # clever and only fetch changesets going forward from the most recent one
+ # it knows about. However, with git, you never know if people have merged
+ # commits into the middle of the repository history, so we always have to
+ # parse the entire log.
+ def fetch_changesets
+ # Save ourselves an expensive operation if we're already up to date
+ return if scm.num_revisions == changesets.count
+
+ revisions = scm.revisions('', nil, nil, :all => true)
+ return if revisions.nil? || revisions.empty?
+
+ # Find revisions that redmine knows about already
+ existing_revisions = changesets.find(:all).map!{|c| c.scmid}
+
+ # Clean out revisions that are no longer in git
+ Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", revisions.map{|r| r.scmid}, self.id])
+
+ # Subtract revisions that redmine already knows about
+ revisions.reject!{|r| existing_revisions.include?(r.scmid)}
+
+ # Save the remaining ones to the database
+ revisions.each{|r| r.save(self)} unless revisions.nil?
+ 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.find(
+ :all,
+ :conditions => [
+ "scmid IN (?)",
+ revisions.map!{|c| c.scmid}
+ ],
+ :order => 'committed_on DESC'
+ )
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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/mercurial_adapter'
+
+class Repository::Mercurial < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::MercurialAdapter
+ end
+
+ def self.scm_name
+ 'Mercurial'
+ end
+
+ def entries(path=nil, identifier=nil)
+ entries=scm.entries(path, identifier)
+ if entries
+ entries.each do |entry|
+ next unless entry.is_file?
+ # Set the filesize unless browsing a specific revision
+ if identifier.nil?
+ full_path = File.join(root_url, entry.path)
+ entry.size = File.stat(full_path).size if File.file?(full_path)
+ end
+ # Search the DB for the entry's last change
+ change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
+ if change
+ entry.lastrev.identifier = change.changeset.revision
+ entry.lastrev.name = change.changeset.revision
+ entry.lastrev.author = change.changeset.committer
+ entry.lastrev.revision = change.revision
+ end
+ end
+ end
+ entries
+ end
+
+ def fetch_changesets
+ scm_info = scm.info
+ if scm_info
+ # latest revision found in database
+ db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
+ # latest revision in the repository
+ latest_revision = scm_info.lastrev
+ return if latest_revision.nil?
+ scm_revision = latest_revision.identifier.to_i
+ if db_revision < scm_revision
+ logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
+ identifier_from = db_revision + 1
+ while (identifier_from <= scm_revision)
+ # loads changesets by batches of 100
+ identifier_to = [identifier_from + 99, scm_revision].min
+ revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
+ transaction do
+ revisions.each do |revision|
+ changeset = Changeset.create(:repository => self,
+ :revision => revision.identifier,
+ :scmid => revision.scmid,
+ :committer => revision.author,
+ :committed_on => revision.time,
+ :comments => revision.message)
+
+ revision.paths.each do |change|
+ Change.create(:changeset => changeset,
+ :action => change[:action],
+ :path => change[:path],
+ :from_path => change[:from_path],
+ :from_revision => change[:from_revision])
+ end
+ end
+ end unless revisions.nil?
+ identifier_from = identifier_to + 1
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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/subversion_adapter'
+
+class Repository::Subversion < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+ validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
+
+ def scm_adapter
+ Redmine::Scm::Adapters::SubversionAdapter
+ end
+
+ def self.scm_name
+ 'Subversion'
+ end
+
+ def latest_changesets(path, rev, limit=10)
+ revisions = scm.revisions(path, nil, nil, :limit => limit)
+ revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
+ end
+
+ # Returns a path relative to the url of the repository
+ def relative_path(path)
+ path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
+ end
+
+ def fetch_changesets
+ scm_info = scm.info
+ if scm_info
+ # latest revision found in database
+ db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
+ # latest revision in the repository
+ scm_revision = scm_info.lastrev.identifier.to_i
+ if db_revision < scm_revision
+ logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
+ identifier_from = db_revision + 1
+ while (identifier_from <= scm_revision)
+ # loads changesets by batches of 200
+ identifier_to = [identifier_from + 199, scm_revision].min
+ revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
+ revisions.reverse_each do |revision|
+ transaction do
+ changeset = Changeset.create(:repository => self,
+ :revision => revision.identifier,
+ :committer => revision.author,
+ :committed_on => revision.time,
+ :comments => revision.message)
+
+ revision.paths.each do |change|
+ Change.create(:changeset => changeset,
+ :action => change[:action],
+ :path => change[:path],
+ :from_path => change[:from_path],
+ :from_revision => change[:from_revision])
+ end unless changeset.new_record?
+ end
+ end unless revisions.nil?
+ identifier_from = identifier_to + 1
+ end
+ end
+ end
+ end
+
+ private
+
+ # Returns the relative url of the repository
+ # Eg: root_url = file:///var/svn/foo
+ # url = file:///var/svn/foo/bar
+ # => returns /bar
+ def relative_url
+ @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Role < ActiveRecord::Base
+ # Built-in roles
+ BUILTIN_NON_MEMBER = 1
+ BUILTIN_ANONYMOUS = 2
+
+ named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
+ named_scope :builtin, lambda { |*args|
+ compare = 'not' if args.first == true
+ { :conditions => "#{compare} builtin = 0" }
+ }
+
+ before_destroy :check_deletable
+ has_many :workflows, :dependent => :delete_all do
+ def copy(role)
+ raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
+ raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
+ clear
+ connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
+ " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
+ " FROM #{Workflow.table_name}" +
+ " WHERE role_id = #{role.id}"
+ end
+ end
+
+ has_many :member_roles, :dependent => :destroy
+ has_many :members, :through => :member_roles
+ acts_as_list
+
+ serialize :permissions, Array
+ attr_protected :builtin
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+ validates_length_of :name, :maximum => 30
+ validates_format_of :name, :with => /^[\w\s\'\-]*$/i
+
+ def permissions
+ read_attribute(:permissions) || []
+ end
+
+ def permissions=(perms)
+ perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
+ write_attribute(:permissions, perms)
+ end
+
+ def add_permission!(*perms)
+ self.permissions = [] unless permissions.is_a?(Array)
+
+ permissions_will_change!
+ perms.each do |p|
+ p = p.to_sym
+ permissions << p unless permissions.include?(p)
+ end
+ save!
+ end
+
+ def remove_permission!(*perms)
+ return unless permissions.is_a?(Array)
+ permissions_will_change!
+ perms.each { |p| permissions.delete(p.to_sym) }
+ save!
+ end
+
+ # Returns true if the role has the given permission
+ def has_permission?(perm)
+ !permissions.nil? && permissions.include?(perm.to_sym)
+ end
+
+ def <=>(role)
+ role ? position <=> role.position : -1
+ end
+
+ def to_s
+ name
+ end
+
+ # Return true if the role is a builtin role
+ def builtin?
+ self.builtin != 0
+ end
+
+ # Return true if the role is a project member role
+ def member?
+ !self.builtin?
+ end
+
+ # Return true if role is allowed to do the specified action
+ # action can be:
+ # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
+ # * a permission Symbol (eg. :edit_project)
+ def allowed_to?(action)
+ if action.is_a? Hash
+ allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
+ else
+ allowed_permissions.include? action
+ end
+ end
+
+ # Return all the permissions that can be given to the role
+ def setable_permissions
+ setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
+ setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
+ setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
+ setable_permissions
+ end
+
+ # Find all the roles that can be given to a project member
+ def self.find_all_givable
+ find(:all, :conditions => {:builtin => 0}, :order => 'position')
+ end
+
+ # Return the builtin 'non member' role
+ def self.non_member
+ find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.')
+ end
+
+ # Return the builtin 'anonymous' role
+ def self.anonymous
+ find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.')
+ end
+
+
+private
+ def allowed_permissions
+ @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
+ end
+
+ def allowed_actions
+ @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
+ end
+
+ def check_deletable
+ raise "Can't delete role" if members.any?
+ raise "Can't delete builtin role" if builtin?
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Setting < ActiveRecord::Base
+
+ DATE_FORMATS = [
+ '%Y-%m-%d',
+ '%d/%m/%Y',
+ '%d.%m.%Y',
+ '%d-%m-%Y',
+ '%m/%d/%Y',
+ '%d %b %Y',
+ '%d %B %Y',
+ '%b %d, %Y',
+ '%B %d, %Y'
+ ]
+
+ TIME_FORMATS = [
+ '%H:%M',
+ '%I:%M %p'
+ ]
+
+ ENCODINGS = %w(US-ASCII
+ windows-1250
+ windows-1251
+ windows-1252
+ windows-1253
+ windows-1254
+ windows-1255
+ windows-1256
+ windows-1257
+ windows-1258
+ windows-31j
+ ISO-2022-JP
+ ISO-2022-KR
+ ISO-8859-1
+ ISO-8859-2
+ ISO-8859-3
+ ISO-8859-4
+ ISO-8859-5
+ ISO-8859-6
+ ISO-8859-7
+ ISO-8859-8
+ ISO-8859-9
+ ISO-8859-13
+ ISO-8859-15
+ KOI8-R
+ UTF-8
+ UTF-16
+ UTF-16BE
+ UTF-16LE
+ EUC-JP
+ Shift_JIS
+ GB18030
+ GBK
+ ISCII91
+ EUC-KR
+ Big5
+ Big5-HKSCS
+ TIS-620)
+
+ cattr_accessor :available_settings
+ @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
+ Redmine::Plugin.all.each do |plugin|
+ next unless plugin.settings
+ @@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
+ end
+
+ validates_uniqueness_of :name
+ validates_inclusion_of :name, :in => @@available_settings.keys
+ validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
+
+ # Hash used to cache setting values
+ @cached_settings = {}
+ @cached_cleared_on = Time.now
+
+ def value
+ v = read_attribute(:value)
+ # Unserialize serialized settings
+ v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
+ v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
+ v
+ end
+
+ def value=(v)
+ v = v.to_yaml if v && @@available_settings[name]['serialized']
+ write_attribute(:value, v.to_s)
+ end
+
+ # Returns the value of the setting named name
+ def self.[](name)
+ v = @cached_settings[name]
+ v ? v : (@cached_settings[name] = find_or_default(name).value)
+ end
+
+ def self.[]=(name, v)
+ setting = find_or_default(name)
+ setting.value = (v ? v : "")
+ @cached_settings[name] = nil
+ setting.save
+ setting.value
+ end
+
+ # Defines getter and setter for each setting
+ # Then setting values can be read using: Setting.some_setting_name
+ # or set using Setting.some_setting_name = "some value"
+ @@available_settings.each do |name, params|
+ src = <<-END_SRC
+ def self.#{name}
+ self[:#{name}]
+ end
+
+ def self.#{name}?
+ self[:#{name}].to_i > 0
+ end
+
+ def self.#{name}=(value)
+ self[:#{name}] = value
+ end
+ END_SRC
+ class_eval src, __FILE__, __LINE__
+ end
+
+ # Helper that returns an array based on per_page_options setting
+ def self.per_page_options_array
+ per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
+ end
+
+ def self.openid?
+ Object.const_defined?(:OpenID) && self[:openid].to_i > 0
+ end
+
+ # Checks if settings have changed since the values were read
+ # and clears the cache hash if it's the case
+ # Called once per request
+ def self.check_cache
+ settings_updated_on = Setting.maximum(:updated_on)
+ if settings_updated_on && @cached_cleared_on <= settings_updated_on
+ @cached_settings.clear
+ @cached_cleared_on = Time.now
+ logger.info "Settings cache cleared." if logger
+ end
+ end
+
+private
+ # Returns the Setting instance for the setting named name
+ # (record found in database or new record with default value)
+ def self.find_or_default(name)
+ name = name.to_s
+ raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
+ setting = find_by_name(name)
+ setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 TimeEntry < ActiveRecord::Base
+ # could have used polymorphic association
+ # project association here allows easy loading of time entries at project level with one database trip
+ belongs_to :project
+ belongs_to :issue
+ belongs_to :user
+ belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
+
+ attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
+
+ acts_as_customizable
+ acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
+ :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
+ :author => :user,
+ :description => :comments
+
+ acts_as_activity_provider :timestamp => "#{table_name}.created_on",
+ :author_key => :user_id,
+ :find_options => {:include => :project}
+
+ validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
+ validates_numericality_of :hours, :allow_nil => true, :message => :invalid
+ validates_length_of :comments, :maximum => 255, :allow_nil => true
+
+ def after_initialize
+ if new_record? && self.activity.nil?
+ if default_activity = TimeEntryActivity.default
+ self.activity_id = default_activity.id
+ end
+ end
+ end
+
+ def before_validation
+ self.project = issue.project if issue && project.nil?
+ end
+
+ def validate
+ errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
+ errors.add :project_id, :invalid if project.nil?
+ errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
+ end
+
+ def hours=(h)
+ write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
+ end
+
+ # tyear, tmonth, tweek assigned where setting spent_on attributes
+ # these attributes make time aggregations easier
+ def spent_on=(date)
+ super
+ self.tyear = spent_on ? spent_on.year : nil
+ self.tmonth = spent_on ? spent_on.month : nil
+ self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
+ end
+
+ # Returns true if the time entry can be edited by usr, otherwise false
+ def editable_by?(usr)
+ (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
+ end
+
+ def self.visible_by(usr)
+ with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
+ yield
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class TimeEntryActivity < Enumeration
+ has_many :time_entries, :foreign_key => 'activity_id'
+
+ OptionName = :enumeration_activities
+ # Backwards compatiblity. Can be removed post-0.9
+ OptName = 'ACTI'
+
+ def option_name
+ OptionName
+ end
+
+ def objects_count
+ time_entries.count
+ end
+
+ def transfer_relations(to)
+ time_entries.update_all("activity_id = #{to.id}")
+ end
+end
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+class TimeEntryActivityCustomField < CustomField\r
+ def type_name\r
+ :enumeration_activities\r
+ end\r
+end\r
+\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class TimeEntryCustomField < CustomField
+ def type_name
+ :label_spent_time
+ end
+end
+
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Token < ActiveRecord::Base
+ belongs_to :user
+ validates_uniqueness_of :value
+
+ before_create :delete_previous_tokens
+
+ @@validity_time = 1.day
+
+ def before_create
+ self.value = Token.generate_token_value
+ end
+
+ # Return true if token has expired
+ def expired?
+ return Time.now > self.created_on + @@validity_time
+ end
+
+ # Delete all expired tokens
+ def self.destroy_expired
+ Token.delete_all ["action <> 'feeds' AND created_on < ?", Time.now - @@validity_time]
+ end
+
+private
+ def self.generate_token_value
+ ActiveSupport::SecureRandom.hex(20)
+ end
+
+ # Removes obsolete tokens (same user and action)
+ def delete_previous_tokens
+ if user
+ Token.delete_all(['user_id = ? AND action = ?', user.id, action])
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Tracker < ActiveRecord::Base
+ before_destroy :check_integrity
+ has_many :issues
+ has_many :workflows, :dependent => :delete_all do
+ def copy(tracker)
+ raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
+ raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
+ clear
+ connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
+ " SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
+ " FROM #{Workflow.table_name}" +
+ " WHERE tracker_id = #{tracker.id}"
+ end
+ end
+
+ has_and_belongs_to_many :projects
+ has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
+ acts_as_list
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+ validates_length_of :name, :maximum => 30
+ validates_format_of :name, :with => /^[\w\s\'\-]*$/i
+
+ def to_s; name end
+
+ def <=>(tracker)
+ name <=> tracker.name
+ end
+
+ def self.all
+ find(:all, :order => 'position')
+ end
+
+private
+ def check_integrity
+ raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require "digest/sha1"
+
+class User < Principal
+
+ # Account statuses
+ STATUS_ANONYMOUS = 0
+ STATUS_ACTIVE = 1
+ STATUS_REGISTERED = 2
+ STATUS_LOCKED = 3
+
+ USER_FORMATS = {
+ :firstname_lastname => '#{firstname} #{lastname}',
+ :firstname => '#{firstname}',
+ :lastname_firstname => '#{lastname} #{firstname}',
+ :lastname_coma_firstname => '#{lastname}, #{firstname}',
+ :username => '#{login}'
+ }
+
+ 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 :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
+ has_many :changesets, :dependent => :nullify
+ has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
+ has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
+ belongs_to :auth_source
+
+ # Active non-anonymous users scope
+ named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
+
+ 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, :group_ids
+
+ 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.blank? }
+ validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
+ # Login must contain lettres, numbers, underscores only
+ validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
+ validates_length_of :login, :maximum => 30
+ validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
+ validates_length_of :firstname, :lastname, :maximum => 30
+ validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
+ validates_length_of :mail, :maximum => 60, :allow_nil => true
+ validates_confirmation_of :password, :allow_nil => true
+
+ def before_create
+ self.mail_notification = false
+ true
+ end
+
+ def before_save
+ # update hashed_password if password was set
+ self.hashed_password = User.hash_password(self.password) if self.password
+ end
+
+ def reload(*args)
+ @name = nil
+ super
+ 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
+ # Invlaid 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)
+ # Make sure no one can sign in with an empty password
+ return nil if password.to_s.empty?
+ user = find(:first, :conditions => ["login=?", login])
+ if user
+ # user is already in local database
+ return nil if !user.active?
+ if user.auth_source
+ # user has an external authentication method
+ return nil unless user.auth_source.authenticate(login, password)
+ else
+ # authentication with local password
+ return nil unless User.hash_password(password) == user.hashed_password
+ end
+ 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 the LDAP") if logger
+ end
+ end
+ end
+ user.update_attribute(: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)
+ tokens = Token.find_all_by_action_and_value('autologin', key)
+ # Make sure there's only 1 token that matches the key
+ if tokens.size == 1
+ token = tokens.first
+ if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
+ token.user.update_attribute(:last_login_on, Time.now)
+ token.user
+ end
+ end
+ end
+
+ # Return user's full name for display
+ def name(formatter = nil)
+ if formatter
+ eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
+ else
+ @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
+ end
+ end
+
+ def active?
+ self.status == STATUS_ACTIVE
+ end
+
+ def registered?
+ self.status == STATUS_REGISTERED
+ end
+
+ def locked?
+ self.status == STATUS_LOCKED
+ end
+
+ def check_password?(clear_password)
+ User.hash_password(clear_password) == self.hashed_password
+ 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
+ token = self.rss_token || Token.create(:user => self, :action => 'feeds')
+ 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 self.find_by_rss_key(key)
+ token = Token.find_by_value(key)
+ token && token.user.active? ? token.user : nil
+ end
+
+ # Makes find_by_mail case-insensitive
+ def self.find_by_mail(mail)
+ find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
+ end
+
+ # Sort users by their display names
+ def <=>(user)
+ self.to_s.downcase <=> user.to_s.downcase
+ end
+
+ def to_s
+ name
+ 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
+
+ def logged?
+ true
+ end
+
+ def anonymous?
+ !logged?
+ end
+
+ # Return user's roles for project
+ def roles_for_project(project)
+ roles = []
+ # No role on archived projects
+ return roles unless project && project.active?
+ if logged?
+ # Find project membership
+ membership = memberships.detect {|m| m.project_id == project.id}
+ 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)
+ !roles_for_project(project).detect {|role| role.member?}.nil?
+ end
+
+ # Return true if the user is allowed to do the specified action on project
+ # action can be:
+ # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
+ # * a permission Symbol (eg. :edit_project)
+ def allowed_to?(action, project, options={})
+ if project
+ # No action allowed on archived projects
+ return false unless project.active?
+ # No action allowed on disabled modules
+ return false unless project.allows_to?(action)
+ # Admin users are authorized for anything else
+ return true if admin?
+
+ roles = roles_for_project(project)
+ return false unless roles
+ roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
+
+ 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.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
+ else
+ false
+ end
+ end
+
+ def self.current=(user)
+ @current_user = user
+ end
+
+ def self.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.find(: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
+
+ protected
+
+ def validate
+ # 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
+
+ # Return password digest
+ def self.hash_password(clear_password)
+ Digest::SHA1.hexdigest(clear_password || "")
+ end
+end
+
+class AnonymousUser < User
+
+ def validate_on_create
+ # There should be only one AnonymousUser in the database
+ errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
+ 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
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class UserCustomField < CustomField
+ def type_name
+ :label_user_plural
+ end
+end
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class UserPreference < ActiveRecord::Base
+ belongs_to :user
+ serialize :others
+
+ attr_protected :others
+
+ def initialize(attributes = nil)
+ super
+ self.others ||= {}
+ end
+
+ def before_save
+ self.others ||= {}
+ end
+
+ def [](attr_name)
+ if attribute_present? attr_name
+ super
+ else
+ others ? others[attr_name] : nil
+ end
+ end
+
+ def []=(attr_name, value)
+ if attribute_present? attr_name
+ super
+ else
+ h = read_attribute(:others).dup || {}
+ h.update(attr_name => value)
+ write_attribute(:others, h)
+ value
+ end
+ end
+
+ def comments_sorting; self[:comments_sorting] end
+ def comments_sorting=(order); self[:comments_sorting]=order end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Version < ActiveRecord::Base
+ before_destroy :check_integrity
+ belongs_to :project
+ has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
+ acts_as_customizable
+ acts_as_attachable :view_permission => :view_files,
+ :delete_permission => :manage_files
+
+ VERSION_STATUSES = %w(open locked closed)
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => [:project_id]
+ validates_length_of :name, :maximum => 60
+ validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
+ validates_inclusion_of :status, :in => VERSION_STATUSES
+
+ named_scope :open, :conditions => {:status => 'open'}
+
+ def start_date
+ effective_date
+ end
+
+ def due_date
+ effective_date
+ end
+
+ # Returns the total estimated time for this version
+ def estimated_hours
+ @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
+ end
+
+ # Returns the total reported time for this version
+ def spent_hours
+ @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
+ end
+
+ def closed?
+ status == 'closed'
+ 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
+
+ # 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_pourcent
+ if issues_count == 0
+ 0
+ elsif open_issues_count == 0
+ 100
+ else
+ issues_progress(false) + issues_progress(true)
+ end
+ end
+
+ # Returns the percentage of issues that have been marked as 'closed'.
+ def closed_pourcent
+ if issues_count == 0
+ 0
+ else
+ issues_progress(false)
+ end
+ 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
+ @issue_count ||= fixed_issues.count
+ end
+
+ # Returns the total amount of open issues for this version.
+ def open_issues_count
+ @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
+ end
+
+ # Returns the total amount of closed issues for this version.
+ def closed_issues_count
+ @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
+ 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
+
+ # 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
+ version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
+ else
+ version.effective_date ? 1 : (self.name <=> version.name)
+ end
+ end
+
+private
+ def check_integrity
+ raise "Can't delete version" if self.fixed_issues.find(:first)
+ 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.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
+ :include => :status,
+ :conditions => ["is_closed = ?", !open]).to_f
+ progress = done / (estimated_average * issues_count)
+ end
+ progress
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software\r
+# Copyright (C) 2006-2009 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+class VersionCustomField < CustomField\r
+ def type_name\r
+ :label_version_plural\r
+ end\r
+end\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Watcher < ActiveRecord::Base
+ belongs_to :watchable, :polymorphic => true
+ belongs_to :user
+
+ validates_presence_of :user
+ validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
+
+ protected
+
+ def validate
+ errors.add :user_id, :invalid unless user.nil? || user.active?
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Wiki < ActiveRecord::Base
+ belongs_to :project
+ has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
+ has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
+
+ acts_as_watchable
+
+ validates_presence_of :start_page
+ validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
+
+ # find the page with the given title
+ # if page doesn't exist, return a new page
+ def find_or_new_page(title)
+ title = start_page if title.blank?
+ find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
+ end
+
+ # find the page with the given title
+ def find_page(title, options = {})
+ title = start_page if title.blank?
+ title = Wiki.titleize(title)
+ page = pages.find_by_title(title)
+ if !page && !(options[:with_redirect] == false)
+ # search for a redirect
+ redirect = redirects.find_by_title(title)
+ page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
+ end
+ page
+ end
+
+ # Finds a page by title
+ # The given string can be of one of the forms: "title" or "project:title"
+ # Examples:
+ # Wiki.find_page("bar", project => foo)
+ # Wiki.find_page("foo:bar")
+ def self.find_page(title, options = {})
+ project = options[:project]
+ if title.to_s =~ %r{^([^\:]+)\:(.*)$}
+ project_identifier, title = $1, $2
+ project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
+ end
+ if project && project.wiki
+ page = project.wiki.find_page(title)
+ if page && page.content
+ page
+ end
+ end
+ end
+
+ # turn a string into a valid page title
+ def self.titleize(title)
+ # replace spaces with _ and remove unwanted caracters
+ title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
+ # upcase the first letter
+ title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
+ title
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'zlib'
+
+class WikiContent < ActiveRecord::Base
+ set_locking_column :version
+ belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+ validates_presence_of :text
+ validates_length_of :comments, :maximum => 255, :allow_nil => true
+
+ acts_as_versioned
+
+ def project
+ page.project
+ end
+
+ class Version
+ belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
+ belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
+ attr_protected :data
+
+ acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
+ :description => :comments,
+ :datetime => :updated_on,
+ :type => 'wiki-page',
+ :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
+
+ acts_as_activity_provider :type => 'wiki_edits',
+ :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
+ :author_key => "#{WikiContent.versioned_table_name}.author_id",
+ :permission => :view_wiki_edits,
+ :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
+ "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
+ "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
+ "#{WikiContent.versioned_table_name}.id",
+ :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
+ "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
+
+ def text=(plain)
+ case Setting.wiki_compression
+ when 'gzip'
+ begin
+ self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
+ self.compression = 'gzip'
+ rescue
+ self.data = plain
+ self.compression = ''
+ end
+ else
+ self.data = plain
+ self.compression = ''
+ end
+ plain
+ end
+
+ def text
+ @text ||= case compression
+ when 'gzip'
+ Zlib::Inflate.inflate(data)
+ else
+ # uncompressed data
+ data
+ end
+ end
+
+ def project
+ page.project
+ end
+
+ # Returns the previous version or nil
+ def previous
+ @previous ||= WikiContent::Version.find(:first,
+ :order => 'version DESC',
+ :include => :author,
+ :conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class WikiContentObserver < ActiveRecord::Observer
+ def after_create(wiki_content)
+ Mailer.deliver_wiki_content_added(wiki_content) if Setting.notified_events.include?('wiki_content_added')
+ end
+
+ def after_update(wiki_content)
+ if wiki_content.text_changed?
+ Mailer.deliver_wiki_content_updated(wiki_content) if Setting.notified_events.include?('wiki_content_updated')
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'diff'
+require 'enumerator'
+
+class WikiPage < ActiveRecord::Base
+ belongs_to :wiki
+ has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
+ acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
+ acts_as_tree :dependent => :nullify, :order => 'title'
+
+ acts_as_watchable
+ acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
+ :description => :text,
+ :datetime => :created_on,
+ :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
+
+ acts_as_searchable :columns => ['title', 'text'],
+ :include => [{:wiki => :project}, :content],
+ :project_key => "#{Wiki.table_name}.project_id"
+
+ attr_accessor :redirect_existing_links
+
+ validates_presence_of :title
+ validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
+ validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
+ validates_associated :content
+
+ def title=(value)
+ value = Wiki.titleize(value)
+ @previous_title = read_attribute(:title) if @previous_title.blank?
+ write_attribute(:title, value)
+ end
+
+ def before_save
+ self.title = Wiki.titleize(title)
+ # Manage redirects if the title has changed
+ if !@previous_title.blank? && (@previous_title != title) && !new_record?
+ # Update redirects that point to the old title
+ wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
+ r.redirects_to = title
+ r.title == r.redirects_to ? r.destroy : r.save
+ end
+ # Remove redirects for the new title
+ wiki.redirects.find_all_by_title(title).each(&:destroy)
+ # Create a redirect to the new title
+ wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
+ @previous_title = nil
+ end
+ end
+
+ def before_destroy
+ # Remove redirects to this page
+ wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
+ end
+
+ def pretty_title
+ WikiPage.pretty_title(title)
+ end
+
+ def content_for_version(version=nil)
+ result = content.versions.find_by_version(version.to_i) if version
+ result ||= content
+ result
+ end
+
+ def diff(version_to=nil, version_from=nil)
+ version_to = version_to ? version_to.to_i : self.content.version
+ version_from = version_from ? version_from.to_i : version_to - 1
+ version_to, version_from = version_from, version_to unless version_from < version_to
+
+ content_to = content.versions.find_by_version(version_to)
+ content_from = content.versions.find_by_version(version_from)
+
+ (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
+ end
+
+ def annotate(version=nil)
+ version = version ? version.to_i : self.content.version
+ c = content.versions.find_by_version(version)
+ c ? WikiAnnotate.new(c) : nil
+ end
+
+ def self.pretty_title(str)
+ (str && str.is_a?(String)) ? str.tr('_', ' ') : str
+ end
+
+ def project
+ wiki.project
+ end
+
+ def text
+ content.text if content
+ end
+
+ # Returns true if usr is allowed to edit the page, otherwise false
+ def editable_by?(usr)
+ !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
+ end
+
+ def attachments_deletable?(usr=User.current)
+ editable_by?(usr) && super(usr)
+ end
+
+ def parent_title
+ @parent_title || (self.parent && self.parent.pretty_title)
+ end
+
+ def parent_title=(t)
+ @parent_title = t
+ parent_page = t.blank? ? nil : self.wiki.find_page(t)
+ self.parent = parent_page
+ end
+
+ protected
+
+ def validate
+ errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
+ errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
+ errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
+ end
+end
+
+class WikiDiff
+ attr_reader :diff, :words, :content_to, :content_from
+
+ def initialize(content_to, content_from)
+ @content_to = content_to
+ @content_from = content_from
+ @words = content_to.text.split(/(\s+)/)
+ @words = @words.select {|word| word != ' '}
+ words_from = content_from.text.split(/(\s+)/)
+ words_from = words_from.select {|word| word != ' '}
+ @diff = words_from.diff @words
+ end
+end
+
+class WikiAnnotate
+ attr_reader :lines, :content
+
+ def initialize(content)
+ @content = content
+ current = content
+ current_lines = current.text.split(/\r?\n/)
+ @lines = current_lines.collect {|t| [nil, nil, t]}
+ positions = []
+ current_lines.size.times {|i| positions << i}
+ while (current.previous)
+ d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
+ d.each_slice(3) do |s|
+ sign, line = s[0], s[1]
+ if sign == '+' && positions[line] && positions[line] != -1
+ if @lines[positions[line]][0].nil?
+ @lines[positions[line]][0] = current.version
+ @lines[positions[line]][1] = current.author
+ end
+ end
+ end
+ d.each_slice(3) do |s|
+ sign, line = s[0], s[1]
+ if sign == '-'
+ positions.insert(line, -1)
+ else
+ positions[line] = nil
+ end
+ end
+ positions.compact!
+ # Stop if every line is annotated
+ break unless @lines.detect { |line| line[0].nil? }
+ current = current.previous
+ end
+ @lines.each { |line| line[0] ||= current.version }
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 WikiRedirect < ActiveRecord::Base
+ belongs_to :wiki
+
+ validates_presence_of :title, :redirects_to
+ validates_length_of :title, :redirects_to, :maximum => 255
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Workflow < ActiveRecord::Base
+ belongs_to :role
+ belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
+ belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
+
+ validates_presence_of :role, :old_status, :new_status
+
+ # Returns workflow transitions count by tracker and role
+ def self.count_by_tracker_and_role
+ counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
+ roles = Role.find(:all, :order => 'builtin, position')
+ trackers = Tracker.find(:all, :order => 'position')
+
+ result = []
+ trackers.each do |tracker|
+ t = []
+ roles.each do |role|
+ row = counts.detect {|c| c['role_id'] == role.id.to_s && c['tracker_id'] == tracker.id.to_s}
+ t << [role, (row.nil? ? 0 : row['c'].to_i)]
+ end
+ result << [tracker, t]
+ end
+
+ result
+ end
+end
--- /dev/null
+<div id="login-form">
+<% form_tag({:action=> "login"}) do %>
+<%= back_url_hidden_field_tag %>
+<table>
+<tr>
+ <td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
+ <td align="left"><p><%= text_field_tag 'username', nil %></p></td>
+</tr>
+<tr>
+ <td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
+ <td align="left"><%= password_field_tag 'password', nil %></td>
+</tr>
+<% if Setting.openid? %>
+<tr>
+ <td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td>
+ <td align="left"><%= text_field_tag "openid_url" %></td>
+</tr>
+<% end %>
+<tr>
+ <td></td>
+ <td align="left">
+ <% if Setting.autologin? %>
+ <label for="autologin"><%= check_box_tag 'autologin' %> <%= l(:label_stay_logged_in) %></label>
+ <% end %>
+ </td>
+</tr>
+<tr>
+ <td align="left">
+ <% if Setting.lost_password? %>
+ <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
+ <% end %>
+ </td>
+ <td align="right">
+ <input type="submit" name="login" value="<%=l(:button_login)%> »" />
+ </td>
+</tr>
+</table>
+<%= javascript_tag "Form.Element.focus('username');" %>
+<% end %>
+</div>
--- /dev/null
+<h2><%=l(:label_password_lost)%></h2>
+
+<div class="box">
+<% form_tag({:action=> "lost_password"}, :class => "tabular") do %>
+
+<p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
+<%= text_field_tag 'mail', nil, :size => 40 %>
+<%= submit_tag l(:button_submit) %></p>
+
+<% end %>
+</div>
--- /dev/null
+<h2><%=l(:label_password_lost)%></h2>
+
+<%= error_messages_for 'user' %>
+
+<% form_tag({:token => @token.value}) do %>
+<div class="box tabular">
+<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
+<%= password_field_tag 'new_password', nil, :size => 25 %><br />
+<em><%= l(:text_caracters_minimum, 4) %></em></p>
+
+<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
+<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
+</div>
+<p><%= submit_tag l(:button_save) %></p>
+<% end %>
--- /dev/null
+<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
+
+<% form_tag({:action => 'register'}, :class => "tabular") do %>
+<%= error_messages_for 'user' %>
+
+<div class="box">
+<!--[form:user]-->
+<% if @user.auth_source_id.nil? %>
+<p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label>
+<%= text_field 'user', 'login', :size => 25 %></p>
+
+<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
+<%= password_field_tag 'password', nil, :size => 25 %><br />
+<em><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
+
+<p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
+<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
+<% end %>
+
+<p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label>
+<%= text_field 'user', 'firstname' %></p>
+
+<p><label for="user_lastname"><%=l(:field_lastname)%> <span class="required">*</span></label>
+<%= text_field 'user', 'lastname' %></p>
+
+<p><label for="user_mail"><%=l(:field_mail)%> <span class="required">*</span></label>
+<%= text_field 'user', 'mail' %></p>
+
+<p><label for="user_language"><%=l(:field_language)%></label>
+<%= select("user", "language", lang_options_for_select) %></p>
+
+<% if Setting.openid? %>
+<p><label for="user_identity_url"><%=l(:field_identity_url)%></label>
+<%= text_field 'user', 'identity_url' %></p>
+<% end %>
+
+<% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>
+ <p><%= custom_field_tag_with_label :user, value %></p>
+<% end %>
+<!--[eoform:user]-->
+</div>
+
+<%= submit_tag l(:button_submit) %>
+<% end %>
--- /dev/null
+<div id="menuAdmin" class="menu" onmouseover="menuMouseover(event)">
+ <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuProjects');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_project_plural)%></span><span class="menuItemArrow">▶</span></a>
+ <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuUsers');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_user_plural)%></span><span class="menuItemArrow">▶</span></a>
+ <%= link_to l(:label_role_and_permissions), {:controller => 'roles' }, :class => "menuItem" %>
+ <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuTrackers');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_issue_tracking)%></span><span class="menuItemArrow">▶</span></a>
+ <%= link_to l(:label_custom_field_plural), {:controller => 'custom_fields' }, :class => "menuItem" %>
+ <%= link_to l(:label_enumerations), {:controller => 'enumerations' }, :class => "menuItem" %>
+ <%= link_to l(:field_mail_notification), {:controller => 'admin', :action => 'mail_options' }, :class => "menuItem" %>
+ <%= link_to l(:label_authentication), {:controller => 'auth_sources' }, :class => "menuItem" %>
+ <%= link_to l(:label_settings), {:controller => 'settings' }, :class => "menuItem" %>
+ <%= link_to l(:label_information_plural), {:controller => 'admin', :action => 'info' }, :class => "menuItem" %>
+</div>
+<div id="menuTrackers" class="menu">
+ <%= link_to l(:label_tracker_plural), {:controller => 'trackers' }, :class => "menuItem" %>
+ <%= link_to l(:label_issue_status_plural), {:controller => 'issue_statuses' }, :class => "menuItem" %>
+ <%= link_to l(:label_workflow), {:controller => 'roles', :action => 'workflow' }, :class => "menuItem" %>
+</div>
+<div id="menuProjects" class="menu">
+ <%= link_to l(:button_list), {:controller => 'admin', :action => 'projects' }, :class => "menuItem" %>
+ <%= link_to l(:label_new), {:controller => 'projects', :action => 'add' }, :class => "menuItem" %>
+</div>
+<div id="menuUsers" class="menu">
+ <%= link_to l(:button_list), {:controller => 'users' }, :class => "menuItem" %>
+ <%= link_to l(:label_new), {:controller => 'users', :action => 'add' }, :class => "menuItem" %>
+</div>
--- /dev/null
+<div class="nodata">
+<% form_tag({:action => 'default_configuration'}) do %>
+ <%= simple_format(l(:text_no_configuration_data)) %>
+ <p><%= l(:field_language) %>:
+ <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
+ <%= submit_tag l(:text_load_default_configuration) %></p>
+<% end %>
+</div>
--- /dev/null
+<h2><%=l(:label_administration)%></h2>
+
+<%= render :partial => 'no_data' if @no_configuration_data %>
+
+<p class="icon22 icon22-projects">
+<%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> |
+<%= link_to l(:label_new), :controller => 'projects', :action => 'add' %>
+</p>
+
+<p class="icon22 icon22-users">
+<%= link_to l(:label_user_plural), :controller => 'users' %> |
+<%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
+</p>
+
+<p class="icon22 icon22-groups">
+<%= link_to l(:label_group_plural), :controller => 'groups' %> |
+<%= link_to l(:label_new), :controller => 'groups', :action => 'new' %>
+</p>
+
+<p class="icon22 icon22-role">
+<%= link_to l(:label_role_and_permissions), :controller => 'roles' %>
+</p>
+
+<p class="icon22 icon22-tracker">
+<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> |
+<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> |
+<%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %>
+</p>
+
+<p class="icon22 icon22-workflow">
+<%= link_to l(:label_custom_field_plural), :controller => 'custom_fields' %>
+</p>
+
+<p class="icon22 icon22-options">
+<%= link_to l(:label_enumerations), :controller => 'enumerations' %>
+</p>
+
+<p class="icon22 icon22-settings">
+<%= link_to l(:label_settings), :controller => 'settings' %>
+</p>
+
+<% menu_items_for(:admin_menu) do |item, caption, url, selected| -%>
+ <%= content_tag 'p',
+ link_to(h(caption), item.url, item.html_options),
+ :class => ["icon22", "icon22-#{item.name}"].join(' ') %>
+<% end -%>
+
+<p class="icon22 icon22-plugin">
+<%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %>
+</p>
+
+<p class="icon22 icon22-info">
+<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
+</p>
+
+<% html_title(l(:label_administration)) -%>
--- /dev/null
+<h2><%=l(:label_information_plural)%></h2>
+
+<p><strong><%= Redmine::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p>
+
+<table class="list">
+<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
+<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
+<tr class="odd"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
+<tr class="even"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
+</table>
+
+<% html_title(l(:label_information_plural)) -%>
--- /dev/null
+<h2><%= l(:label_plugins) %></h2>
+
+<% if @plugins.any? %>
+<table class="list plugins">
+ <% @plugins.each do |plugin| %>
+ <tr class="<%= cycle('odd', 'even') %>">
+ <td><span class="name"><%=h plugin.name %></span>
+ <%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %>
+ <%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %>
+ </td>
+ <td class="author"><%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %></td>
+ <td class="version"><%=h plugin.version %></td>
+ <td class="configure"><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %></td>
+ </tr>
+ <% end %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_project_plural)%></h2>
+
+<% form_tag({}, :method => :get) do %>
+<fieldset><legend><%= l(:label_filter_plural) %></legend>
+<label><%= l(:field_status) %> :</label>
+<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
+<label><%= l(:label_project) %>:</label>
+<%= text_field_tag 'name', params[:name], :size => 30 %>
+<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
+</fieldset>
+<% end %>
+
+
+<table class="list">
+ <thead><tr>
+ <th><%=l(:label_project)%></th>
+ <th><%=l(:field_description)%></th>
+ <th><%=l(:field_is_public)%></th>
+ <th><%=l(:field_created_on)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% for project in @projects %>
+ <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
+ <td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
+ <td><%= textilizable project.short_description, :project => project %></td>
+ <td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
+ <td align="center"><%= format_date(project.created_on) %></td>
+ <td class="buttons">
+ <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
+ <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
+ <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
+ <%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %>
+ </td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<% html_title(l(:label_project_plural)) -%>
--- /dev/null
+<span id="attachments_fields">
+<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%>
+<%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %>
+<em><%= l(:label_optional_description) %></em>
+</span>
+<br />
+<small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
+(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
+</small>
--- /dev/null
+<div class="attachments">
+<% for attachment in attachments %>
+<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
+<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
+ <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
+ <% if options[:deletable] %>
+ <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
+ :confirm => l(:text_are_you_sure),
+ :method => :post,
+ :class => 'delete',
+ :title => l(:button_delete) %>
+ <% end %>
+ <% if options[:author] %>
+ <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
+ <% end %>
+ </p>
+<% end %>
+</div>
--- /dev/null
+<h2><%=h @attachment.filename %></h2>
+
+<div class="attachments">
+<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
+ <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
+<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
+ <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
+
+</div>
+
+<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
+
+<% html_title @attachment.filename %>
+
+<% content_for :header_tags do -%>
+ <%= stylesheet_link_tag "scm" -%>
+<% end -%>
--- /dev/null
+<h2><%=h @attachment.filename %></h2>
+
+<div class="attachments">
+<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
+ <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
+<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
+ <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
+
+</div>
+
+<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %>
+
+<% html_title @attachment.filename %>
+
+<% content_for :header_tags do -%>
+ <%= stylesheet_link_tag "scm" -%>
+<% end -%>
--- /dev/null
+<%= error_messages_for 'auth_source' %>
+
+<div class="box">
+<!--[form:auth_source]-->
+<p><label for="auth_source_name"><%=l(:field_name)%> <span class="required">*</span></label>
+<%= text_field 'auth_source', 'name' %></p>
+
+<p><label for="auth_source_host"><%=l(:field_host)%> <span class="required">*</span></label>
+<%= text_field 'auth_source', 'host' %></p>
+
+<p><label for="auth_source_port"><%=l(:field_port)%> <span class="required">*</span></label>
+<%= text_field 'auth_source', 'port', :size => 6 %> <%= check_box 'auth_source', 'tls' %> LDAPS</p>
+
+<p><label for="auth_source_account"><%=l(:field_account)%></label>
+<%= text_field 'auth_source', 'account' %></p>
+
+<p><label for="auth_source_account_password"><%=l(:field_password)%></label>
+<%= password_field 'auth_source', 'account_password', :name => 'ignore',
+ :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]';" %></p>
+
+<p><label for="auth_source_base_dn"><%=l(:field_base_dn)%> <span class="required">*</span></label>
+<%= text_field 'auth_source', 'base_dn', :size => 60 %></p>
+
+<p><label for="auth_source_onthefly_register"><%=l(:field_onthefly)%></label>
+<%= check_box 'auth_source', 'onthefly_register' %></p>
+</div>
+
+<fieldset class="box"><legend><%=l(:label_attribute_plural)%></legend>
+<p><label for="auth_source_attr_login"><%=l(:field_login)%> <span class="required">*</span></label>
+<%= text_field 'auth_source', 'attr_login', :size => 20 %></p>
+
+<p><label for="auth_source_attr_firstname"><%=l(:field_firstname)%></label>
+<%= text_field 'auth_source', 'attr_firstname', :size => 20 %></p>
+
+<p><label for="auth_source_attr_lastname"><%=l(:field_lastname)%></label>
+<%= text_field 'auth_source', 'attr_lastname', :size => 20 %></p>
+
+<p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label>
+<%= text_field 'auth_source', 'attr_mail', :size => 20 %></p>
+</fieldset>
+<!--[eoform:auth_source]-->
+
--- /dev/null
+<h2><%=l(:label_auth_source)%> (<%= @auth_source.auth_method_name %>)</h2>
+
+<% form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
+
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_auth_source_plural)%></h2>
+
+<table class="list">
+ <thead><tr>
+ <th><%=l(:field_name)%></th>
+ <th><%=l(:field_type)%></th>
+ <th><%=l(:field_host)%></th>
+ <th><%=l(:label_user_plural)%></th>
+ <th></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% for source in @auth_sources %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to source.name, :action => 'edit', :id => source%></td>
+ <td align="center"><%= source.auth_method_name %></td>
+ <td align="center"><%= source.host %></td>
+ <td align="center"><%= source.users.count %></td>
+ <td align="center"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td>
+ <td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source },
+ :confirm => l(:text_are_you_sure),
+ :class => "button-small",
+ :disabled => source.users.any? %></td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<p class="pagination"><%= pagination_links_full @auth_source_pages %></p>
--- /dev/null
+<h2><%=l(:label_auth_source_new)%> (<%= @auth_source.auth_method_name %>)</h2>
+
+<% form_tag({:action => 'create'}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<%= error_messages_for 'board' %>
+
+<!--[form:board]-->
+<div class="box">
+<p><%= f.text_field :name, :required => true %></p>
+<p><%= f.text_field :description, :required => true, :size => 80 %></p>
+</div>
+<!--[eoform:board]-->
--- /dev/null
+<h2><%= l(:label_board) %></h2>
+
+<% labelled_tabular_form_for :board, @board, :url => {:action => 'edit', :id => @board} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<h2><%= l(:label_board_plural) %></h2>
+
+<table class="list boards">
+ <thead><tr>
+ <th><%= l(:label_board) %></th>
+ <th><%= l(:label_topic_plural) %></th>
+ <th><%= l(:label_message_plural) %></th>
+ <th><%= l(:label_message_last) %></th>
+ </tr></thead>
+ <tbody>
+<% for board in @boards %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td>
+ <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %><br />
+ <%=h board.description %>
+ </td>
+ <td align="center"><%= board.topics_count %></td>
+ <td align="center"><%= board.messages_count %></td>
+ <td>
+ <small>
+ <% if board.last_message %>
+ <%= authoring board.last_message.created_on, board.last_message.author %><br />
+ <%= link_to_message board.last_message %>
+ <% end %>
+ </small>
+ </td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_messages => 1, :key => User.current.rss_key} %>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>
+<% end %>
+
+<% html_title l(:label_board_plural) %>
--- /dev/null
+<h2><%= l(:label_board_new) %></h2>
+
+<% labelled_tabular_form_for :board, @board, :url => {:action => 'new'} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %>
+
+<div class="contextual">
+<%= link_to_if_authorized l(:label_message_new),
+ {:controller => 'messages', :action => 'new', :board_id => @board},
+ :class => 'icon icon-add',
+ :onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %>
+<%= watcher_tag(@board, User.current) %>
+</div>
+
+<div id="add-message" style="display:none;">
+<% if authorize_for('messages', 'new') %>
+<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %></h2>
+<% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
+ <%= render :partial => 'messages/form', :locals => {:f => f} %>
+ <p><%= submit_tag l(:button_create) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('message-form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %> |
+ <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
+<% end %>
+<div id="preview" class="wiki"></div>
+<% end %>
+</div>
+
+<h2><%=h @board.name %></h2>
+<p class="subtitle"><%=h @board.description %></p>
+
+<% if @topics.any? %>
+<table class="list messages">
+ <thead><tr>
+ <th><%= l(:field_subject) %></th>
+ <th><%= l(:field_author) %></th>
+ <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
+ <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
+ <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
+ </tr></thead>
+ <tbody>
+ <% @topics.each do |topic| %>
+ <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
+ <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td>
+ <td class="author" align="center"><%= topic.author %></td>
+ <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
+ <td class="replies" align="center"><%= topic.replies_count %></td>
+ <td class="last_message">
+ <% if topic.last_reply %>
+ <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
+ <%= link_to_message topic.last_reply %>
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
+<% end %>
+
+<% html_title h(@board.name) %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
+<% end %>
--- /dev/null
+<h2>403</h2>
+
+<p><%= l(:notice_not_authorized) %></p>
+<p><a href="javascript:history.back()">Back</a></p>
+
+<% html_title '403' %>
--- /dev/null
+<h2>404</h2>
+
+<p><%= l(:notice_file_not_found) %></p>
+<p><a href="javascript:history.back()">Back</a></p>
+
+<% html_title '404' %>
--- /dev/null
+<table class="cal">
+<thead>
+<tr><td></td><% 7.times do |i| %><th><%= day_name( (calendar.first_wday+i)%7 ) %></th><% end %></tr>
+</thead>
+<tbody>
+<tr>
+<% day = calendar.startdt
+while day <= calendar.enddt %>
+<%= "<th>#{day.cweek}</th>" if day.cwday == calendar.first_wday %>
+<td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>">
+<p class="day-num"><%= day.day %></p>
+<% calendar.events_on(day).each do |i| %>
+ <% if i.is_a? Issue %>
+ <div class="<%= i.css_classes %> tooltip">
+ <%= if day == i.start_date && day == i.due_date
+ image_tag('arrow_bw.png')
+ elsif day == i.start_date
+ image_tag('arrow_from.png')
+ elsif day == i.due_date
+ image_tag('arrow_to.png')
+ end %>
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_issue i, :truncate => 30 %>
+ <span class="tip"><%= render_issue_tooltip i %></span>
+ </div>
+ <% else %>
+ <span class="icon icon-package">
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_version i%>
+ </span>
+ <% end %>
+<% end %>
+</td>
+<%= '</tr><tr>' if day.cwday==calendar.last_wday and day!=calendar.enddt %>
+<% day = day + 1
+end %>
+</tr>
+</tbody>
+</table>
--- /dev/null
+<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%>
+<% diff.each do |table_file| -%>
+<div class="autoscroll">
+<% if diff_type == 'sbs' -%>
+<table class="filecontent CodeRay">
+<thead>
+<tr><th colspan="4" class="filename"><%= table_file.file_name %></th></tr>
+</thead>
+<tbody>
+<% prev_line_left, prev_line_right = nil, nil -%>
+<% table_file.keys.sort.each do |key| -%>
+<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
+<tr class="spacing">
+<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td>
+<% end -%>
+<tr>
+ <th class="line-num"><%= table_file[key].nb_line_left %></th>
+ <td class="line-code <%= table_file[key].type_diff_left %>">
+ <pre><%=to_utf8 table_file[key].line_left %></pre>
+ </td>
+ <th class="line-num"><%= table_file[key].nb_line_right %></th>
+ <td class="line-code <%= table_file[key].type_diff_right %>">
+ <pre><%=to_utf8 table_file[key].line_right %></pre>
+ </td>
+</tr>
+<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%>
+<% end -%>
+</tbody>
+</table>
+
+<% else -%>
+<table class="filecontent CodeRay">
+<thead>
+<tr><th colspan="3" class="filename"><%= table_file.file_name %></th></tr>
+</thead>
+<tbody>
+<% prev_line_left, prev_line_right = nil, nil -%>
+<% table_file.keys.sort.each do |key, line| %>
+<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
+<tr class="spacing">
+<th class="line-num">...</th><th class="line-num">...</th><td></td>
+</tr>
+<% end -%>
+<tr>
+ <th class="line-num"><%= table_file[key].nb_line_left %></th>
+ <th class="line-num"><%= table_file[key].nb_line_right %></th>
+ <% if table_file[key].line_left.empty? -%>
+ <td class="line-code <%= table_file[key].type_diff_right %>">
+ <pre><%=to_utf8 table_file[key].line_right %></pre>
+ </td>
+ <% else -%>
+ <td class="line-code <%= table_file[key].type_diff_left %>">
+ <pre><%=to_utf8 table_file[key].line_left %></pre>
+ </td>
+ <% end -%>
+</tr>
+<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%>
+<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%>
+<% end -%>
+</tbody>
+</table>
+<% end -%>
+
+</div>
+<% end -%>
+
+<%= l(:text_diff_truncated) if diff.truncated? %>
--- /dev/null
+<div class="autoscroll">
+<table class="filecontent CodeRay">
+<tbody>
+<% line_num = 1 %>
+<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %>
+<tr><th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th><td class="line-code"><pre><%= line %></pre></td></tr>
+<% line_num += 1 %>
+<% end %>
+</tbody>
+</table>
+</div>
--- /dev/null
+<fieldset class="preview"><legend><%= l(:label_preview) %></legend>
+<%= textilizable @text, :attachments => @attachements, :object => @previewed %>
+</fieldset>
--- /dev/null
+<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %>
+
+<div class="tabs">
+ <ul>
+ <% tabs.each do |tab| -%>
+ <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
+ :id => "tab-#{tab[:name]}",
+ :class => (tab[:name] != selected_tab ? nil : 'selected'),
+ :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
+ <% end -%>
+ </ul>
+</div>
+
+<% 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 -%>
--- /dev/null
+xml.instruct!
+xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
+ xml.title truncate_single_line(@title, :length => 100)
+ xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
+ xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
+ xml.id url_for(:controller => 'welcome', :only_path => false)
+ xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
+ xml.author { xml.name "#{Setting.app_title}" }
+ xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
+ @items.each do |item|
+ xml.entry do
+ url = url_for(item.event_url(:only_path => false))
+ if @project
+ xml.title truncate_single_line(item.event_title, :length => 100)
+ else
+ xml.title truncate_single_line("#{item.project} - #{item.event_title}", :length => 100)
+ end
+ xml.link "rel" => "alternate", "href" => url
+ xml.id url
+ xml.updated item.event_datetime.xmlschema
+ author = item.event_author if item.respond_to?(:event_author)
+ xml.author do
+ xml.name(author)
+ xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
+ end if author
+ xml.content "type" => "html" do
+ xml.text! textilizable(item, :event_description, :only_path => false)
+ end
+ end
+ end
+end
--- /dev/null
+<%= error_messages_for 'custom_field' %>
+
+<script type="text/javascript">
+//<![CDATA[
+function toggle_custom_field_format() {
+ format = $("custom_field_field_format");
+ p_length = $("custom_field_min_length");
+ p_regexp = $("custom_field_regexp");
+ p_values = $("custom_field_possible_values");
+ p_searchable = $("custom_field_searchable");
+ p_default = $("custom_field_default_value");
+
+ p_default.setAttribute('type','text');
+ Element.show(p_default.parentNode);
+
+ switch (format.value) {
+ case "list":
+ Element.hide(p_length.parentNode);
+ Element.hide(p_regexp.parentNode);
+ if (p_searchable) Element.show(p_searchable.parentNode);
+ Element.show(p_values);
+ break;
+ case "bool":
+ p_default.setAttribute('type','checkbox');
+ Element.hide(p_length.parentNode);
+ Element.hide(p_regexp.parentNode);
+ if (p_searchable) Element.hide(p_searchable.parentNode);
+ Element.hide(p_values);
+ break;
+ case "date":
+ Element.hide(p_length.parentNode);
+ Element.hide(p_regexp.parentNode);
+ if (p_searchable) Element.hide(p_searchable.parentNode);
+ Element.hide(p_values);
+ break;
+ case "float":
+ case "int":
+ Element.show(p_length.parentNode);
+ Element.show(p_regexp.parentNode);
+ if (p_searchable) Element.hide(p_searchable.parentNode);
+ Element.hide(p_values);
+ break;
+ default:
+ Element.show(p_length.parentNode);
+ Element.show(p_regexp.parentNode);
+ if (p_searchable) Element.show(p_searchable.parentNode);
+ Element.hide(p_values);
+ break;
+ }
+}
+
+//]]>
+</script>
+
+<div class="box">
+<p><%= f.text_field :name, :required => true %></p>
+<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();",
+ :disabled => !@custom_field.new_record? %></p>
+<p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
+ <%= f.text_field :min_length, :size => 5, :no_label => true %> -
+ <%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p>
+<p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p>
+<p id="custom_field_possible_values"><%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"),
+ :cols => 20,
+ :rows => 15 %>
+<br /><em><%= l(:text_custom_field_possible_values_info) %></em></p>
+<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
+<%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
+</div>
+
+<div class="box">
+<% case @custom_field.class.name
+when "IssueCustomField" %>
+
+ <fieldset><legend><%=l(:label_tracker_plural)%></legend>
+ <% for tracker in @trackers %>
+ <%= check_box_tag "custom_field[tracker_ids][]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %>
+ <% end %>
+ <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
+ </fieldset>
+
+ <p><%= f.check_box :is_required %></p>
+ <p><%= f.check_box :is_for_all %></p>
+ <p><%= f.check_box :is_filter %></p>
+ <p><%= f.check_box :searchable %></p>
+
+<% when "UserCustomField" %>
+ <p><%= f.check_box :is_required %></p>
+ <p><%= f.check_box :editable %></p>
+
+<% when "ProjectCustomField" %>
+ <p><%= f.check_box :is_required %></p>
+
+<% when "TimeEntryCustomField" %>
+ <p><%= f.check_box :is_required %></p>
+
+<% else %>
+ <p><%= f.check_box :is_required %></p>
+
+<% end %>
+<%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
+</div>
+<%= javascript_tag "toggle_custom_field_format();" %>
--- /dev/null
+<table class="list">
+ <thead><tr>
+ <th width="30%"><%=l(:field_name)%></th>
+ <th><%=l(:field_field_format)%></th>
+ <th><%=l(:field_is_required)%></th>
+ <% if tab[:name] == 'IssueCustomField' %>
+ <th><%=l(:field_is_for_all)%></th>
+ <th><%=l(:label_used_by)%></th>
+ <% end %>
+ <th><%=l(:button_sort)%></th>
+ <th width="10%"></th>
+ </tr></thead>
+ <tbody>
+ <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td>
+ <td align="center"><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %></td>
+ <td align="center"><%= image_tag 'true.png' if custom_field.is_required? %></td>
+ <% if tab[:name] == 'IssueCustomField' %>
+ <td align="center"><%= image_tag 'true.png' if custom_field.is_for_all? %></td>
+ <td align="center"><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
+ <% end %>
+ <td align="center" style="width:15%;"><%= reorder_links('custom_field', {:action => 'edit', :id => custom_field}) %></td>
+ <td class="buttons">
+ <%= link_to(l(:button_delete), { :action => 'destroy', :id => custom_field },
+ :method => :post,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del') %>
+ </td>
+ </tr>
+ <% end; reset_cycle %>
+ </tbody>
+</table>
+
+<p><%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %></p>
--- /dev/null
+<h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.type %>
+ » <%=h @custom_field.name %> (<%=l(@custom_field.type_name)%>)</h2>
+
+<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "edit", :id => @custom_field } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_custom_field_plural)%></h2>
+
+<%= render_tabs custom_fields_tabs %>
+
+<% html_title(l(:label_custom_field_plural)) -%>
--- /dev/null
+<h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.type %>
+ » <%=l(:label_custom_field_new)%> (<%=l(@custom_field.type_name)%>)</h2>
+
+<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "new" } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= hidden_field_tag 'type', @custom_field.type %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<p><%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %><br />
+<% unless document.description.blank? %><%=h(truncate(document.description, :length => 250)) %><br /><% end %>
+<em><%= format_time(document.created_on) %></em></p>
\ No newline at end of file
--- /dev/null
+<%= error_messages_for 'document' %>
+<div class="box">
+<!--[form:document]-->
+<p><label for="document_category_id"><%=l(:field_category)%></label>
+<%= select('document', 'category_id', DocumentCategory.all.collect {|c| [c.name, c.id]}) %></p>
+
+<p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label>
+<%= text_field 'document', 'title', :size => 60 %></p>
+
+<p><label for="document_description"><%=l(:field_description)%></label>
+<%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
+<!--[eoform:document]-->
+</div>
+
+<%= wikitoolbar_for 'document_description' %>
--- /dev/null
+<h2><%=l(:label_document)%></h2>
+
+<% form_tag({:action => 'edit', :id => @document}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
+
+
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:label_document_new),
+ {:controller => 'documents', :action => 'new', :project_id => @project},
+ :class => 'icon icon-add',
+ :onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' %>
+</div>
+
+<div id="add-document" style="display:none;">
+<h2><%=l(:label_document_new)%></h2>
+<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %>
+<%= render :partial => 'documents/form' %>
+<div class="box">
+<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
+</div>
+<%= submit_tag l(:button_create) %>
+<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %>
+<% end %>
+</div>
+
+<h2><%=l(:label_document_plural)%></h2>
+
+<% if @grouped.empty? %><p class="nodata"><%= l(:label_no_data) %></p><% end %>
+
+<% @grouped.keys.sort.each do |group| %>
+ <h3><%= group %></h3>
+ <%= render :partial => 'documents/document', :collection => @grouped[group] %>
+<% end %>
+
+<% content_for :sidebar do %>
+ <h3><%= l(:label_sort_by, '') %></h3>
+ <% form_tag({}, :method => :get) do %>
+ <label><%= radio_button_tag 'sort_by', 'category', (@sort_by == 'category'), :onclick => 'this.form.submit();' %> <%= l(:field_category) %></label><br />
+ <label><%= radio_button_tag 'sort_by', 'date', (@sort_by == 'date'), :onclick => 'this.form.submit();' %> <%= l(:label_date) %></label><br />
+ <label><%= radio_button_tag 'sort_by', 'title', (@sort_by == 'title'), :onclick => 'this.form.submit();' %> <%= l(:field_title) %></label><br />
+ <label><%= radio_button_tag 'sort_by', 'author', (@sort_by == 'author'), :onclick => 'this.form.submit();' %> <%= l(:field_author) %></label>
+ <% end %>
+<% end %>
+
+<% html_title(l(:label_document_plural)) -%>
--- /dev/null
+<h2><%=l(:label_document_new)%></h2>
+
+<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %>
+<%= render :partial => 'documents/form' %>
+
+<div class="box">
+<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
+</div>
+
+<%= submit_tag l(:button_create) %>
+<% end %>
+
+
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
+<%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+</div>
+
+<h2><%=h @document.title %></h2>
+
+<p><em><%=h @document.category.name %><br />
+<%= format_date @document.created_on %></em></p>
+<div class="wiki">
+<%= textilizable @document.description, :attachments => @document.attachments %>
+</div>
+
+<h3><%= l(:label_attachment_plural) %></h3>
+<%= link_to_attachments @document %>
+
+<% if authorize_for('documents', 'add_attachment') %>
+<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
+ :id => 'attach_files_link' %></p>
+ <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
+ <div class="box">
+ <p><%= render :partial => 'attachments/form' %></p>
+ </div>
+ <%= submit_tag l(:button_add) %>
+ <% end %>
+<% end %>
+
+<% html_title @document.title -%>
--- /dev/null
+<%= error_messages_for 'enumeration' %>
+<div class="box">
+<!--[form:optvalue]-->
+<%= hidden_field 'enumeration', 'type' %>
+
+<p><label for="enumeration_name"><%=l(:field_name)%></label>
+<%= text_field 'enumeration', 'name' %></p>
+
+<p><label for="enumeration_active"><%=l(:field_active)%></label>
+<%= check_box 'enumeration', 'active' %></p>
+
+<p><label for="enumeration_is_default"><%=l(:field_is_default)%></label>
+<%= check_box 'enumeration', 'is_default' %></p>
+<!--[eoform:optvalue]-->
+
+<% @enumeration.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :enumeration, value %></p>
+<% end %>
+</div>
--- /dev/null
+<h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
+
+<% form_tag({}) do %>
+<div class="box">
+<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
+<p><%= l(:text_enumeration_category_reassign_to) %>
+<%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
+</div>
+
+<%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %>
+<% end %>
--- /dev/null
+<h2><%= link_to l(@enumeration.option_name), :controller => 'enumerations', :action => 'index' %> » <%=h @enumeration %></h2>
+
+<% form_tag({:action => 'update', :id => @enumeration}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
+
+<% form_tag({:action => 'destroy', :id => @enumeration}) do %>
+ <%= submit_tag l(:button_delete) %>
+<% end %>
\ No newline at end of file
--- /dev/null
+<h2><%=l(:label_enumerations)%></h2>
+
+<% Enumeration.get_subclasses.each do |klass| %>
+<h3><%= l(klass::OptionName) %></h3>
+
+<% enumerations = klass.shared %>
+<% if enumerations.any? %>
+<table class="list">
+<tr>
+ <th><%= l(:field_name) %></th>
+ <th style="width:15%;"><%= l(:field_is_default) %></th>
+ <th style="width:15%;"><%= l(:field_active) %></th>
+ <th style="width:15%;"></th>
+ <th align="center" style="width:10%;"> </th>
+</tr>
+<% enumerations.each do |enumeration| %>
+<tr class="<%= cycle('odd', 'even') %>">
+ <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td>
+ <td class="center" style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td>
+ <td class="center" style="width:15%;"><%= image_tag('true.png') if enumeration.active? %></td>
+ <td style="width:15%;"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}) %></td>
+ <td class="buttons">
+ <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration },
+ :method => :post,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del' %>
+ </td>
+</tr>
+<% end %>
+</table>
+<% reset_cycle %>
+<% end %>
+
+<p><%= link_to l(:label_enumeration_new), { :action => 'new', :type => klass.name } %></p>
+<% end %>
+
+<% html_title(l(:label_enumerations)) -%>
--- /dev/null
+<h2><%= link_to l(@enumeration.option_name), :controller => 'enumerations', :action => 'index' %> » <%=l(:label_enumeration_new)%></h2>
+
+<% form_tag({:action => 'create'}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<%= error_messages_for :group %>
+
+<div class="box tabular">
+ <p><%= f.text_field :lastname, :label => :field_name %></p>
+ <% @group.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :group, value %></p>
+ <% end %>
+</div>
--- /dev/null
+<% labelled_tabular_form_for :group, @group, :url => group_path(@group), :html => {:method => :put} do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% roles = Role.find_all_givable %>
+<% projects = Project.active.find(:all, :order => 'lft') %>
+
+<div class="splitcontentleft">
+<% if @group.memberships.any? %>
+<table class="list memberships">
+ <thead>
+ <th><%= l(:label_project) %></th>
+ <th><%= l(:label_role_plural) %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+ <% @group.memberships.each do |membership| %>
+ <% next if membership.new_record? %>
+ <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
+ <td class="project"><%=h membership.project %></td>
+ <td class="roles">
+ <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
+ <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
+ :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
+ <p><% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
+ <% end %></p>
+ <p><%= submit_tag l(:button_change) %>
+ <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
+ <% end %>
+ </td>
+ <td class="buttons">
+ <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
+ <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership },
+ :method => :post },
+ :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ </tbody>
+<% end; reset_cycle %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<% if projects.any? %>
+<fieldset><legend><%=l(:label_project_new)%></legend>
+<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
+<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
+<p><%= l(:label_role_plural) %>:
+<% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
+<% end %></p>
+<p><%= submit_tag l(:button_add) %></p>
+<% end %>
+</fieldset>
+<% end %>
+</div>
--- /dev/null
+<div class="splitcontentleft">
+<% if @group.users.any? %>
+ <table class="list users">
+ <thead>
+ <th><%= l(:label_user) %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+ <% @group.users.sort.each do |user| %>
+ <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
+ <td class="user"><%= link_to_user user %></td>
+ <td class="buttons">
+ <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'remove_user', :id => @group, :user_id => user },
+ :method => :post },
+ :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<% users = User.active.find(:all, :limit => 100) - @group.users %>
+<% if users.any? %>
+ <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
+ <fieldset><legend><%=l(:label_user_new)%></legend>
+
+ <p><%= text_field_tag 'user_search', nil, :size => "40" %></p>
+ <%= observe_field(:user_search,
+ :frequency => 0.5,
+ :update => :users,
+ :url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
+ :with => 'q')
+ %>
+
+ <div id="users">
+ <%= principals_check_box_tags 'user_ids[]', users %>
+ </div>
+
+ <p><%= submit_tag l(:button_add) %></p>
+ </fieldset>
+ <% end %>
+<% end %>
+
+</div>
--- /dev/null
+<%= principals_check_box_tags 'user_ids[]', @users %>
--- /dev/null
+<h2><%= link_to l(:label_group_plural), groups_path %> » <%= h(@group) %></h2>
+
+<%= render_tabs group_settings_tabs %>
+
+<% html_title(l(:label_group), @group, l(:label_administration)) -%>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
+</div>
+
+<h2><%= l(:label_group_plural) %></h2>
+
+<% if @groups.any? %>
+<table class="list groups">
+ <thead><tr>
+ <th><%=l(:label_group)%></th>
+ <th><%=l(:label_user_plural)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% @groups.each do |group| %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%= link_to h(group), :action => 'edit', :id => group %></td>
+ <td align="center"><%= group.users.size %></td>
+ <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
+ </tr>
+<% end %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
--- /dev/null
+<h2><%= link_to l(:label_group_plural), groups_path %> » <%= l(:label_group_new) %></h2>
+
+<%= error_messages_for :group %>
+
+<% form_for(@group, :builder => TabularFormBuilder, :lang => current_language) do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<p><%= f.submit l(:button_create) %></p>
+<% end %>
--- /dev/null
+<h2><%= link_to l(:label_group_plural), groups_path %> » <%=h @group %></h2>
+
+<ul>
+<% @group.users.each do |user| %>
+ <li><%=h user %></li>
+<% end %>
+</ul>
--- /dev/null
+<%= error_messages_for 'category' %>
+
+<div class="box">
+<p><%= f.text_field :name, :size => 30, :required => true %></p>
+<p><%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
+</div>
--- /dev/null
+<h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2>
+
+<% form_tag({}) do %>
+<div class="box">
+<p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
+<p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
+<% if @categories.size > 0 %>
+<label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %></label>:
+<%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %></p>
+<% end %>
+</div>
+
+<%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_issue_category)%></h2>
+
+<% labelled_tabular_form_for :category, @category, :url => { :action => 'edit', :id => @category } do |f| %>
+<%= render :partial => 'issue_categories/form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<%= error_messages_for 'relation' %>
+
+<p><%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %>
+<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %>
+<span id="predecessor_fields" style="display:none;">
+<%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %>
+</span>
+<%= submit_tag l(:button_add) %>
+<%= toggle_link l(:button_cancel), 'new-relation-form'%>
+</p>
+
+<%= javascript_tag "setPredecessorFieldsVisibility();" %>
--- /dev/null
+<%= error_messages_for 'issue_status' %>
+
+<div class="box">
+<!--[form:issue_status]-->
+<p><label for="issue_status_name"><%=l(:field_name)%><span class="required"> *</span></label>
+<%= text_field 'issue_status', 'name' %></p>
+
+<p><label for="issue_status_is_closed"><%=l(:field_is_closed)%></label>
+<%= check_box 'issue_status', 'is_closed' %></p>
+
+<p><label for="issue_status_is_default"><%=l(:field_is_default)%></label>
+<%= check_box 'issue_status', 'is_default' %></p>
+
+<%= call_hook(:view_issue_statuses_form, :issue_status => @issue_status) %>
+
+<!--[eoform:issue_status]-->
+</div>
--- /dev/null
+<h2><%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses', :action => 'index' %> » <%=h @issue_status %></h2>
+
+<% form_tag({:action => 'update', :id => @issue_status}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_issue_status_plural)%></h2>
+
+<table class="list">
+ <thead><tr>
+ <th><%=l(:field_status)%></th>
+ <th><%=l(:field_is_default)%></th>
+ <th><%=l(:field_is_closed)%></th>
+ <th><%=l(:button_sort)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% for status in @issue_statuses %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to status.name, :action => 'edit', :id => status %></td>
+ <td align="center"><%= image_tag 'true.png' if status.is_default? %></td>
+ <td align="center"><%= image_tag 'true.png' if status.is_closed? %></td>
+ <td align="center" style="width:15%;"><%= reorder_links('issue_status', {:action => 'update', :id => status}) %></td>
+ <td class="buttons">
+ <%= link_to(l(:button_delete), { :action => 'destroy', :id => status },
+ :method => :post,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del') %>
+ </td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<p class="pagination"><%= pagination_links_full @issue_status_pages %></p>
+
+<% html_title(l(:label_issue_status_plural)) -%>
--- /dev/null
+<h2><%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses', :action => 'index' %> » <%=l(:label_issue_status_new)%></h2>
+
+<% form_tag({:action => 'create'}, :class => "tabular") do %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<% changesets.each do |changeset| %>
+ <div class="changeset <%= cycle('odd', 'even') %>">
+ <p><%= link_to("#{l(:label_revision)} #{changeset.revision}",
+ :controller => 'repositories', :action => 'revision', :id => changeset.project, :rev => changeset.revision) %><br />
+ <span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
+ <%= textilizable(changeset, :comments) %>
+ </div>
+<% end %>
--- /dev/null
+<% labelled_tabular_form_for :issue, @issue,
+ :url => {:action => 'edit', :id => @issue},
+ :html => {:id => 'issue-form',
+ :class => nil,
+ :multipart => true} do |f| %>
+ <%= error_messages_for 'issue' %>
+ <%= error_messages_for 'time_entry' %>
+ <div class="box">
+ <% if @edit_allowed || !@allowed_statuses.empty? %>
+ <fieldset class="tabular"><legend><%= l(:label_change_properties) %>
+ <% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %>
+ <small>(<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>)</small>
+ <% end %>
+ </legend>
+ <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %>
+ </fieldset>
+ <% end %>
+ <% if authorize_for('timelog', 'edit') %>
+ <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
+ <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %>
+ <div class="splitcontentleft">
+ <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
+ </div>
+ <div class="splitcontentright">
+ <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
+ </div>
+ <p><%= time_entry.text_field :comments, :size => 60 %></p>
+ <% @time_entry.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :time_entry, value %></p>
+ <% end %>
+ <% end %>
+ </fieldset>
+ <% end %>
+
+ <fieldset><legend><%= l(:field_notes) %></legend>
+ <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
+ <%= wikitoolbar_for 'notes' %>
+ <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
+
+ <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
+ </fieldset>
+ </div>
+
+ <%= f.hidden_field :lock_version %>
+ <%= submit_tag l(:button_submit) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue },
+ :method => 'post',
+ :update => 'preview',
+ :with => 'Form.serialize("issue-form")',
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+
+<div id="preview" class="wiki"></div>
--- /dev/null
+<% if @issue.new_record? %>
+<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
+<%= observe_field :issue_tracker_id, :url => { :action => :new },
+ :update => :content,
+ :with => "Form.serialize('issue-form')" %>
+<hr />
+<% end %>
+
+<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
+<p><%= f.text_field :subject, :size => 80, :required => true %></p>
+<p><%= f.text_area :description,
+ :cols => 60,
+ :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
+ :accesskey => accesskey(:edit),
+ :class => 'wiki-edit' %></p>
+</div>
+
+<div class="attributes">
+<div class="splitcontentleft">
+<% if @issue.new_record? || @allowed_statuses.any? %>
+<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
+<% else %>
+<p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
+<% end %>
+
+<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
+<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
+<% unless @project.issue_categories.empty? %>
+<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
+<%= prompt_to_remote(l(:label_issue_category_new),
+ l(:label_issue_category_new), 'category[name]',
+ {:controller => 'projects', :action => 'add_issue_category', :id => @project},
+ :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
+<% end %>
+<% unless @issue.assignable_versions.empty? %>
+<p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
+<p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
+<p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
+<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
+</div>
+
+<div style="clear:both;"> </div>
+<%= render :partial => 'form_custom_fields' %>
+</div>
+
+<% if @issue.new_record? %>
+<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
+<% end %>
+
+<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
+<p><label><%= l(:label_issue_watchers) %></label>
+<% @issue.project.users.sort.each do |user| -%>
+<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
+<% end -%>
+</p>
+<% end %>
+
+<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
+
+<%= wikitoolbar_for 'issue_description' %>
--- /dev/null
+<div class="splitcontentleft">
+<% i = 0 %>
+<% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %>
+<% @issue.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :issue, value %></p>
+<% if i == split_on -%>
+</div><div class="splitcontentright">
+<% end -%>
+<% i += 1 -%>
+<% end -%>
+</div>
+<div style="clear:both;"> </div>
--- /dev/null
+<div class="attributes">
+<div class="splitcontentleft">
+<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
+<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
+</div>
+<div class="splitcontentright">
+<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
+<% unless @issue.assignable_versions.empty? %>
+<p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
+<% end %>
+</div>
+</div>
--- /dev/null
+<% reply_links = authorize_for('issues', 'edit') -%>
+<% for journal in journals %>
+ <div id="change-<%= journal.id %>" class="journal">
+ <h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
+ <%= content_tag('a', '', :name => "note-#{journal.indice}")%>
+ <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
+ <%= avatar(journal.user, :size => "32") %>
+ <ul>
+ <% for detail in journal.details %>
+ <li><%= show_detail(detail) %></li>
+ <% end %>
+ </ul>
+ <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %>
+ </div>
+ <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
+<% end %>
--- /dev/null
+<% form_tag({}) do -%>
+<%= hidden_field_tag 'back_url', url_for(params) %>
+<table class="list issues">
+ <thead><tr>
+ <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
+ </th>
+ <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
+ <% query.columns.each do |column| %>
+ <%= column_header(column) %>
+ <% end %>
+ </tr></thead>
+ <% previous_group = false %>
+ <tbody>
+ <% issues.each do |issue| -%>
+ <% if @query.grouped? && (group = column_value(@query.group_by_column, issue) || '') != previous_group %>
+ <% reset_cycle %>
+ <tr class="group open">
+ <td colspan="<%= query.columns.size + 2 %>">
+ <span class="expander" onclick="toggleRowGroup(this); return false;"> </span>
+ <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
+ </td>
+ </tr>
+ <% previous_group = group %>
+ <% end %>
+ <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
+ <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
+ <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
+ <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
+ </tr>
+ <% end -%>
+ </tbody>
+</table>
+<% end -%>
--- /dev/null
+<% if issues && issues.any? %>
+<% form_tag({}) do %>
+ <table class="list issues">
+ <thead><tr>
+ <th>#</th>
+ <th><%=l(:field_project)%></th>
+ <th><%=l(:field_tracker)%></th>
+ <th><%=l(:field_subject)%></th>
+ </tr></thead>
+ <tbody>
+ <% for issue in issues %>
+ <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
+ <td class="id">
+ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
+ <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
+ </td>
+ <td class="project"><%= link_to(h(issue.project), :controller => 'projects', :action => 'show', :id => issue.project) %></td>
+ <td class="tracker"><%=h issue.tracker %></td>
+ <td class="subject">
+ <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+<% end %>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
--- /dev/null
+<div class="contextual">
+<% if authorize_for('issue_relations', 'new') %>
+ <%= toggle_link l(:button_add), 'new-relation-form'%>
+<% end %>
+</div>
+
+<p><strong><%=l(:label_related_issues)%></strong></p>
+
+<% if @issue.relations.any? %>
+<table style="width:100%">
+<% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %>
+<tr>
+<td><%= l(relation.label_for(@issue)) %> <%= "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)})" if relation.delay && relation.delay != 0 %>
+ <%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %>
+ <%= link_to_issue relation.other_issue(@issue) %>
+</td>
+<td><%= relation.other_issue(@issue).status.name %></td>
+<td><%= format_date(relation.other_issue(@issue).start_date) %></td>
+<td><%= format_date(relation.other_issue(@issue).due_date) %></td>
+<td><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation},
+ :method => :post
+ }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %></td>
+</tr>
+<% end %>
+</table>
+<% end %>
+
+<% remote_form_for(:relation, @relation,
+ :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue},
+ :method => :post,
+ :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %>
+<%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
+<% end %>
--- /dev/null
+<h3><%= l(:label_issue_plural) %></h3>
+<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
+<% if @project %>
+<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
+<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br />
+<% end %>
+<%= call_hook(:view_issues_sidebar_issues_bottom) %>
+
+<% planning_links = []
+ planning_links << link_to(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true)
+ planning_links << link_to(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true)
+%>
+<% unless planning_links.empty? %>
+<h3><%= l(:label_planning) %></h3>
+<p><%= planning_links.join(' | ') %></p>
+<%= call_hook(:view_issues_sidebar_planning_bottom) %>
+<% end %>
+
+<% unless sidebar_queries.empty? -%>
+<h3><%= l(:label_query_plural) %></h3>
+
+<% sidebar_queries.each do |query| -%>
+<%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br />
+<% end -%>
+<%= call_hook(:view_issues_sidebar_queries_bottom) %>
+<% end -%>
--- /dev/null
+<h2><%= l(:label_bulk_edit_selected_issues) %></h2>
+
+<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
+
+<% form_tag() do %>
+<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
+<div class="box">
+<fieldset>
+<legend><%= l(:label_change_properties) %></legend>
+<p>
+<% if @available_statuses.any? %>
+<label><%= l(:field_status) %>:
+<%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
+<% end %>
+<label><%= l(:field_priority) %>:
+<%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.all, :id, :name)) %></label>
+<label><%= l(:field_category) %>:
+<%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
+ content_tag('option', l(:label_none), :value => 'none') +
+ options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
+</p>
+<p>
+<label><%= l(:field_assigned_to) %>:
+<%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
+ content_tag('option', l(:label_nobody), :value => 'none') +
+ options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
+<label><%= l(:field_fixed_version) %>:
+<%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
+ content_tag('option', l(:label_none), :value => 'none') +
+ options_from_collection_for_select(@project.versions.open.sort, :id, :name)) %></label>
+</p>
+
+<p>
+<label><%= l(:field_start_date) %>:
+<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
+<label><%= l(:field_due_date) %>:
+<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
+<label><%= l(:field_done_ratio) %>:
+<%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
+</p>
+
+<% @custom_fields.each do |custom_field| %>
+<p><label><%= h(custom_field.name) %></label>
+<%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
+</p>
+<% end %>
+
+<%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
+</fieldset>
+
+<fieldset><legend><%= l(:field_notes) %></legend>
+<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
+<%= wikitoolbar_for 'notes' %>
+</fieldset>
+</div>
+
+<p><%= submit_tag l(:button_submit) %>
+<% end %>
--- /dev/null
+<h2><%= l(:label_calendar) %></h2>
+
+<% form_tag({}, :id => 'query_form') do %>
+<fieldset id="filters" class="collapsible">
+ <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
+ <div>
+ <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
+ </div>
+</fieldset>
+
+<p style="float:right;">
+<%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")),
+ {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }},
+ {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))}
+ %> |
+<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'),
+ {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }},
+ {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))}
+ %>
+</p>
+
+<p class="buttons">
+<%= select_month(@month, :prefix => "month", :discard_type => true) %>
+<%= select_year(@year, :prefix => "year", :discard_type => true) %>
+
+<%= link_to_remote l(:button_apply),
+ { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
+ :update => "content",
+ :with => "Form.serialize('query_form')"
+ }, :class => 'icon icon-checked' %>
+
+<%= link_to_remote l(:button_clear),
+ { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
+ :update => "content",
+ }, :class => 'icon icon-reload' if @query.new_record? %>
+</p>
+<% end %>
+
+<%= error_messages_for 'query' %>
+<% if @query.valid? %>
+<%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
+
+<%= image_tag 'arrow_from.png' %> <%= l(:text_tip_task_begin_day) %><br />
+<%= image_tag 'arrow_to.png' %> <%= l(:text_tip_task_end_day) %><br />
+<%= image_tag 'arrow_bw.png' %> <%= l(:text_tip_task_begin_end_day) %><br />
+<% end %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'issues/sidebar' %>
+<% end %>
+
+<% html_title(l(:label_calendar)) -%>
--- /dev/null
+xml.instruct!
+xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
+ xml.title @title
+ xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false)
+ xml.link "rel" => "alternate", "href" => home_url(:only_path => false)
+ xml.id url_for(:controller => 'welcome', :only_path => false)
+ xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema)
+ xml.author { xml.name "#{Setting.app_title}" }
+ @journals.each do |change|
+ issue = change.issue
+ xml.entry do
+ xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
+ xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false)
+ xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false)
+ xml.updated change.created_on.xmlschema
+ xml.author do
+ xml.name change.user.name
+ xml.email(change.user.mail) if change.user.is_a?(User) && !change.user.mail.blank? && !change.user.pref.hide_mail
+ end
+ xml.content "type" => "html" do
+ xml.text! '<ul>'
+ change.details.each do |detail|
+ xml.text! '<li>' + show_detail(detail, false) + '</li>'
+ end
+ xml.text! '</ul>'
+ xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+<ul>
+ <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
+
+<% if !@issue.nil? -%>
+ <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
+ :class => 'icon-edit', :disabled => !@can[:edit] %></li>
+ <li class="folder">
+ <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
+ <ul>
+ <% @statuses.each do |s| -%>
+ <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post,
+ :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
+ <% end -%>
+ </ul>
+ </li>
+<% else %>
+ <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
+ :class => 'icon-edit', :disabled => !@can[:edit] %></li>
+<% end %>
+
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_priority) %></a>
+ <ul>
+ <% @priorities.each do |p| -%>
+ <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post,
+ :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li>
+ <% end -%>
+ </ul>
+ </li>
+ <% unless @project.nil? || @project.versions.open.empty? -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
+ <ul>
+ <% @project.versions.open.sort.each do |v| -%>
+ <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
+ :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
+ <% end -%>
+ <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
+ </ul>
+ </li>
+ <% end %>
+ <% unless @assignables.nil? || @assignables.empty? -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
+ <ul>
+ <% @assignables.each do |u| -%>
+ <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post,
+ :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
+ <% end -%>
+ <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
+ </ul>
+ </li>
+ <% end %>
+ <% unless @project.nil? || @project.issue_categories.empty? -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_category) %></a>
+ <ul>
+ <% @project.issue_categories.each do |u| -%>
+ <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post,
+ :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
+ <% end -%>
+ <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
+ </ul>
+ </li>
+ <% end -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
+ <ul>
+ <% (0..10).map{|x|x*10}.each do |p| -%>
+ <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
+ :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
+ <% end -%>
+ </ul>
+ </li>
+
+<% if !@issue.nil? %>
+ <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
+ :class => 'icon-copy', :disabled => !@can[:copy] %></li>
+ <% if @can[:log_time] -%>
+ <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
+ :class => 'icon-time-add' %></li>
+ <% end %>
+ <% if User.current.logged? %>
+ <li><%= watcher_link(@issue, User.current) %></li>
+ <% end %>
+<% end %>
+
+ <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
+ :class => 'icon-move', :disabled => !@can[:move] %></li>
+ <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
+ :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
+
+ <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
+</ul>
--- /dev/null
+<h2><%= l(:label_confirmation) %></h2>
+
+<% form_tag do %>
+<%= @issues.collect {|i| hidden_field_tag 'ids[]', i.id } %>
+<div class="box">
+<p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
+<p>
+<label><%= radio_button_tag 'todo', 'destroy', true %> <%= l(:text_destroy_time_entries) %></label><br />
+<label><%= radio_button_tag 'todo', 'nullify', false %> <%= l(:text_assign_time_entries_to_project) %></label><br />
+<label><%= radio_button_tag 'todo', 'reassign', false, :onchange => 'if (this.checked) { $("reassign_to_id").focus(); }' %> <%= l(:text_reassign_time_entries) %></label>
+<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %>
+</p>
+</div>
+<%= submit_tag l(:button_apply) %>
+<% end %>
--- /dev/null
+<h2><%=h "#{@issue.tracker.name} ##{@issue.id}" %></h2>
+
+<%= render :partial => 'edit' %>
--- /dev/null
+<h2><%= l(:label_gantt) %></h2>
+
+<% form_tag(params.merge(:month => nil, :year => nil, :months => nil), :id => 'query_form') do %>
+<fieldset id="filters" class="collapsible">
+ <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
+ <div>
+ <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
+ </div>
+</fieldset>
+
+<p style="float:right;">
+<%= if @gantt.zoom < 4
+ link_to_remote image_tag('zoom_in.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom+1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom+1)))}
+ else
+ image_tag 'zoom_in_g.png'
+ end %>
+<%= if @gantt.zoom > 1
+ link_to_remote image_tag('zoom_out.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom-1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom-1)))}
+ else
+ image_tag 'zoom_out_g.png'
+ end %>
+</p>
+
+<p class="buttons">
+<%= text_field_tag 'months', @gantt.months, :size => 2 %>
+<%= l(:label_months_from) %>
+<%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
+<%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
+<%= hidden_field_tag 'zoom', @gantt.zoom %>
+
+<%= link_to_remote l(:button_apply),
+ { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
+ :update => "content",
+ :with => "Form.serialize('query_form')"
+ }, :class => 'icon icon-checked' %>
+
+<%= link_to_remote l(:button_clear),
+ { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
+ :update => "content",
+ }, :class => 'icon icon-reload' if @query.new_record? %>
+</p>
+<% end %>
+
+<%= error_messages_for 'query' %>
+<% if @query.valid? %>
+<% zoom = 1
+@gantt.zoom.times { zoom = zoom * 2 }
+
+subject_width = 330
+header_heigth = 18
+
+headers_height = header_heigth
+show_weeks = false
+show_days = false
+
+if @gantt.zoom >1
+ show_weeks = true
+ headers_height = 2*header_heigth
+ if @gantt.zoom > 2
+ show_days = true
+ headers_height = 3*header_heigth
+ end
+end
+
+g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
+g_height = [(20 * @gantt.events.length + 6)+150, 206].max
+t_height = g_height + headers_height
+%>
+
+<table width="100%" style="border:0; border-collapse: collapse;">
+<tr>
+<td style="width:<%= subject_width %>px; padding:0px;">
+
+<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
+<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
+<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
+<%
+#
+# Tasks subjects
+#
+top = headers_height + 8
+@gantt.events.each do |i| %>
+ <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
+ <% if i.is_a? Issue %>
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_issue i %>
+ <% else %>
+ <span class="icon icon-package">
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_version i %>
+ </span>
+ <% end %>
+ </small></div>
+ <% top = top + 20
+end %>
+</div>
+</td>
+<td>
+
+<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
+<div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"> </div>
+<%
+#
+# Months headers
+#
+month_f = @gantt.date_from
+left = 0
+height = (show_weeks ? header_heigth : header_heigth + g_height)
+@gantt.months.times do
+ width = ((month_f >> 1) - month_f) * zoom - 1
+ %>
+ <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
+ <%= link_to "#{month_f.year}-#{month_f.month}", @gantt.params.merge(:year => month_f.year, :month => month_f.month), :title => "#{month_name(month_f.month)} #{month_f.year}"%>
+ </div>
+ <%
+ left = left + width + 1
+ month_f = month_f >> 1
+end %>
+
+<%
+#
+# Weeks headers
+#
+if show_weeks
+ left = 0
+ height = (show_days ? header_heigth-1 : header_heigth-1 + g_height)
+ if @gantt.date_from.cwday == 1
+ # @date_from is monday
+ week_f = @gantt.date_from
+ else
+ # find next monday after @date_from
+ week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
+ width = (7 - @gantt.date_from.cwday + 1) * zoom-1
+ %>
+ <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr"> </div>
+ <%
+ left = left + width+1
+ end %>
+ <%
+ while week_f <= @gantt.date_to
+ width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom -1 : (@gantt.date_to - week_f + 1) * zoom-1
+ %>
+ <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
+ <small><%= week_f.cweek if width >= 16 %></small>
+ </div>
+ <%
+ left = left + width+1
+ week_f = week_f+7
+ end
+end %>
+
+<%
+#
+# Days headers
+#
+if show_days
+ left = 0
+ height = g_height + header_heigth - 1
+ wday = @gantt.date_from.cwday
+ (@gantt.date_to - @gantt.date_from + 1).to_i.times do
+ width = zoom - 1
+ %>
+ <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
+ <%= day_name(wday).first %>
+ </div>
+ <%
+ left = left + width+1
+ wday = wday + 1
+ wday = 1 if wday > 7
+ end
+end %>
+
+<%
+#
+# Tasks
+#
+top = headers_height + 10
+@gantt.events.each do |i|
+ if i.is_a? Issue
+ i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
+ i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
+
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
+ i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
+
+ i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
+
+ i_left = ((i_start_date - @gantt.date_from)*zoom).floor
+ i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
+ d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
+ l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
+ %>
+ <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo"> </div>
+ <% if l_width > 0 %>
+ <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late"> </div>
+ <% end %>
+ <% if d_width > 0 %>
+ <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done"> </div>
+ <% end %>
+ <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task">
+ <%= i.status.name %>
+ <%= (i.done_ratio).to_i %>%
+ </div>
+ <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
+ <span class="tip">
+ <%= render_issue_tooltip i %>
+ </span></div>
+<% else
+ i_left = ((i.start_date - @gantt.date_from)*zoom).floor
+ %>
+ <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone"> </div>
+ <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <strong><%=h i %></strong>
+ </div>
+<% end %>
+ <% top = top + 20
+end %>
+
+<%
+#
+# Today red line (excluded from cache)
+#
+if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
+ <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= ((Date.today-@gantt.date_from+1)*zoom).floor()-1 %>px;width:10px;border-left: 1px dashed red;"> </div>
+<% end %>
+
+</div>
+</td>
+</tr>
+</table>
+
+<table width="100%">
+<tr>
+<td align="left"><%= link_to_remote ('« ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
+<td align="right"><%= link_to_remote (l(:label_next) + ' »'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
+</tr>
+</table>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'PDF', :url => @gantt.params %>
+ <%= f.link_to('PNG', :url => @gantt.params) if @gantt.respond_to?('to_image') %>
+<% end %>
+<% end # query.valid? %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'issues/sidebar' %>
+<% end %>
+
+<% html_title(l(:label_gantt)) -%>
--- /dev/null
+<div class="contextual">
+<% if !@query.new_record? && @query.editable_by?(User.current) %>
+ <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
+ <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+<% end %>
+</div>
+
+<h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
+<% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
+
+<% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
+ <%= hidden_field_tag('project_id', @project.to_param) if @project %>
+ <div id="query_form_content">
+ <fieldset id="filters" class="collapsible">
+ <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
+ <div>
+ <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
+ </div>
+ </fieldset>
+ <fieldset class="collapsible collapsed">
+ <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
+ <div style="display: none;">
+ <%= l(:field_group_by) %>
+ <%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %>
+ </div>
+ </fieldset>
+ </div>
+ <p class="buttons">
+
+ <%= link_to_remote l(:button_apply),
+ { :url => { :set_filter => 1 },
+ :update => "content",
+ :with => "Form.serialize('query_form')"
+ }, :class => 'icon icon-checked' %>
+
+ <%= link_to_remote l(:button_clear),
+ { :url => { :set_filter => 1, :project_id => @project },
+ :method => :get,
+ :update => "content",
+ }, :class => 'icon icon-reload' %>
+
+ <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
+ <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
+ <% end %>
+ </p>
+<% end %>
+
+<%= error_messages_for 'query' %>
+<% if @query.valid? %>
+<% if @issues.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% else %>
+<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
+<p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => { :project_id => @project, :query_id => (@query.new_record? ? nil : @query), :key => User.current.rss_key } %>
+ <%= f.link_to 'CSV', :url => { :project_id => @project } %>
+ <%= f.link_to 'PDF', :url => { :project_id => @project } %>
+<% end %>
+
+<% end %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'issues/sidebar' %>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
+ <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
+ <%= javascript_include_tag 'context_menu' %>
+ <%= stylesheet_link_tag 'context_menu' %>
+<% end %>
+
+<div id="context-menu" style="display: none;"></div>
+<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
--- /dev/null
+<h2><%= l(:button_move) %></h2>
+
+<ul>
+<% @issues.each do |issue| -%>
+ <li><%= link_to_issue issue %></li>
+<% end -%>
+</ul>
+
+<% form_tag({}, :id => 'move_form') do %>
+<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
+
+<div class="box tabular">
+<p><label for="new_project_id"><%=l(:field_project)%>:</label>
+<%= select_tag "new_project_id",
+ project_tree_options_for_select(@allowed_projects, :selected => @target_project),
+ :onchange => remote_function(:url => { :action => 'move' },
+ :method => :get,
+ :update => 'content',
+ :with => "Form.serialize('move_form')") %></p>
+
+<p><label for="new_tracker_id"><%=l(:field_tracker)%>:</label>
+<%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
+
+<p><label for="copy_options_copy"><%= l(:button_copy)%></label>
+<%= check_box_tag "copy_options[copy]", "1" %></p>
+</div>
+
+<%= submit_tag l(:button_move) %>
+<%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_issue_new)%></h2>
+
+<% labelled_tabular_form_for :issue, @issue,
+ :html => {:multipart => true, :id => 'issue-form'} do |f| %>
+ <%= error_messages_for 'issue' %>
+ <div class="box">
+ <%= render :partial => 'issues/form', :locals => {:f => f} %>
+ </div>
+ <%= submit_tag l(:button_create) %>
+ <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'issues', :action => 'preview', :project_id => @project },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('issue-form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %>
+
+ <%= javascript_tag "Form.Element.focus('issue_subject');" %>
+<% end %>
+
+<div id="preview" class="wiki"></div>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
+<%= watcher_tag(@issue, User.current) %>
+<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
+<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
+<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+</div>
+
+<h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
+
+<div class="<%= @issue.css_classes %> details">
+ <%= avatar(@issue.author, :size => "64") %>
+ <h3><%=h @issue.subject %></h3>
+ <p class="author">
+ <%= authoring @issue.created_on, @issue.author %>.
+ <% if @issue.created_on != @issue.updated_on %>
+ <%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
+ <% end %>
+ </p>
+
+<table class="attributes">
+<tr>
+ <th class="status"><%=l(:field_status)%>:</th><td class="status"><%= @issue.status.name %></td>
+ <th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
+</tr>
+<tr>
+ <th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= @issue.priority.name %></td>
+ <th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
+</tr>
+<tr>
+ <th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
+ <th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
+</tr>
+<tr>
+ <th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h @issue.category ? @issue.category.name : "-" %></td>
+ <% if User.current.allowed_to?(:view_time_entries, @project) %>
+ <th class="spent-time"><%=l(:label_spent_time)%>:</th>
+ <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
+ <% end %>
+</tr>
+<tr>
+ <th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
+ <% if @issue.estimated_hours %>
+ <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
+ <% end %>
+</tr>
+<%= render_custom_fields_rows(@issue) %>
+<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
+</table>
+<hr />
+
+<div class="contextual">
+<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
+</div>
+
+<p><strong><%=l(:field_description)%></strong></p>
+<div class="wiki">
+<%= textilizable @issue, :description, :attachments => @issue.attachments %>
+</div>
+
+<%= link_to_attachments @issue %>
+
+<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
+
+<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
+<hr />
+<div id="relations">
+<%= render :partial => 'relations' %>
+</div>
+<% end %>
+
+<% if User.current.allowed_to?(:add_issue_watchers, @project) ||
+ (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
+<hr />
+<div id="watchers">
+<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
+</div>
+<% end %>
+
+</div>
+
+<% if @changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
+<div id="issue-changesets">
+<h3><%=l(:label_associated_revisions)%></h3>
+<%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
+</div>
+<% end %>
+
+<% if @journals.any? %>
+<div id="history">
+<h3><%=l(:label_history)%></h3>
+<%= render :partial => 'history', :locals => { :journals => @journals } %>
+</div>
+<% end %>
+<div style="clear: both;"></div>
+
+<% if authorize_for('issues', 'edit') %>
+ <div id="update" style="display:none;">
+ <h3><%= l(:button_update) %></h3>
+ <%= render :partial => 'edit' %>
+ </div>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
+ <%= f.link_to 'PDF' %>
+<% end %>
+
+<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'issues/sidebar' %>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
--- /dev/null
+<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
+ <%= text_area_tag :notes, @journal.notes, :class => 'wiki-edit',
+ :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
+ <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
+ <p><%= submit_tag l(:button_save) %>
+ <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
+ "Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
+<% end %>
--- /dev/null
+page.hide "journal-#{@journal.id}-notes"
+page.insert_html :after, "journal-#{@journal.id}-notes",
+ :partial => 'notes_form'
--- /dev/null
+if @journal.frozen?
+ # journal was destroyed
+ page.remove "change-#{@journal.id}"
+else
+ page.replace "journal-#{@journal.id}-notes", render_notes(@journal, :reply_links => authorize_for('issues', 'edit'))
+ page.show "journal-#{@journal.id}-notes"
+ page.remove "journal-#{@journal.id}-form"
+end
+
+call_hook(:view_journals_update_rjs_bottom, { :page => page, :journal => @journal })
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title><%=h html_title %></title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<meta name="description" content="<%= Redmine::Info.app_name %>" />
+<meta name="keywords" content="issue,bug,tracker" />
+<%= stylesheet_link_tag 'application', :media => 'all' %>
+<%= javascript_include_tag :defaults %>
+<%= heads_for_wiki_formatter %>
+<!--[if IE]>
+ <style type="text/css">
+ * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
+ body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
+ </style>
+<![endif]-->
+<%= call_hook :view_layouts_base_html_head %>
+<!-- page specific tags -->
+<%= yield :header_tags -%>
+</head>
+<body>
+<div id="wrapper">
+<div id="top-menu">
+ <div id="account">
+ <%= render_menu :account_menu -%>
+ </div>
+ <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
+ <%= render_menu :top_menu -%>
+</div>
+
+<div id="header">
+ <div id="quick-search">
+ <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
+ <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
+ <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
+ <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
+ <% end %>
+ <%= render_project_jump_box %>
+ </div>
+
+ <h1><%= page_header_title %></h1>
+
+ <div id="main-menu">
+ <%= render_main_menu(@project) %>
+ </div>
+</div>
+
+<%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
+ <div id="sidebar">
+ <%= yield :sidebar %>
+ <%= call_hook :view_layouts_base_sidebar %>
+ </div>
+
+ <div id="content">
+ <%= render_flash_messages %>
+ <%= yield %>
+ <%= call_hook :view_layouts_base_content %>
+ <div style="clear:both;"></div>
+ </div>
+</div>
+
+<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
+
+<div id="footer">
+ Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> © 2006-2009 Jean-Philippe Lang
+</div>
+</div>
+<%= call_hook :view_layouts_base_body_bottom %>
+</body>
+</html>
--- /dev/null
+<html>
+<head>
+<style>
+body {
+ font-family: Verdana, sans-serif;
+ font-size: 0.8em;
+ color:#484848;
+}
+h1, h2, h3 { font-family: "Trebuchet MS", Verdana, sans-serif; margin: 0px; }
+h1 { font-size: 1.2em; }
+h2, h3 { font-size: 1.1em; }
+a, a:link, a:visited { color: #2A5685;}
+a:hover, a:active { color: #c61a1a; }
+a.wiki-anchor { display: none; }
+hr {
+ width: 100%;
+ height: 1px;
+ background: #ccc;
+ border: 0;
+}
+.footer {
+ font-size: 0.8em;
+ font-style: italic;
+}
+</style>
+</head>
+<body>
+<%= yield %>
+<hr />
+<span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span>
+</body>
+</html>
--- /dev/null
+<%= yield %>
+--
+<%= Setting.emails_footer %>
--- /dev/null
+<h1><%= link_to "#{issue.tracker.name} ##{issue.id}: #{issue.subject}", issue_url %></h1>
+
+<ul>
+<li><%=l(:field_author)%>: <%= issue.author %></li>
+<li><%=l(:field_status)%>: <%= issue.status %></li>
+<li><%=l(:field_priority)%>: <%= issue.priority %></li>
+<li><%=l(:field_assigned_to)%>: <%= issue.assigned_to %></li>
+<li><%=l(:field_category)%>: <%= issue.category %></li>
+<li><%=l(:field_fixed_version)%>: <%= issue.fixed_version %></li>
+<% issue.custom_values.each do |c| %>
+ <li><%= c.custom_field.name %>: <%= show_value(c) %></li>
+<% end %>
+</ul>
+
+<%= textilizable(issue, :description, :only_path => false) %>
--- /dev/null
+<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %>
+<%= issue_url %>
+
+<%=l(:field_author)%>: <%= issue.author %>
+<%=l(:field_status)%>: <%= issue.status %>
+<%=l(:field_priority)%>: <%= issue.priority %>
+<%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
+<%=l(:field_category)%>: <%= issue.category %>
+<%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
+<% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %>
+<% end %>
+
+<%= issue.description %>
--- /dev/null
+<p><%= l(:notice_account_activated) %></p>
+<p><%= l(:label_login) %>: <%= link_to @login_url, @login_url %></p>
--- /dev/null
+<%= l(:notice_account_activated) %>
+<%= l(:label_login) %>: <%= @login_url %>
--- /dev/null
+<p><%= l(:mail_body_account_activation_request, @user.login) %></p>
+<p><%= link_to @url, @url %></p>
--- /dev/null
+<%= l(:mail_body_account_activation_request, @user.login) %>
+<%= @url %>
--- /dev/null
+<% if @user.auth_source %>
+<p><%= l(:mail_body_account_information_external, @user.auth_source.name) %></p>
+<% else %>
+<p><%= l(:mail_body_account_information) %>:</p>
+<ul>
+ <li><%= l(:field_login) %>: <%= @user.login %></li>
+ <li><%= l(:field_password) %>: <%= @password %></li>
+</ul>
+<% end %>
+
+<p><%= l(:label_login) %>: <%= auto_link(@login_url) %></p>
--- /dev/null
+<% if @user.auth_source %><%= l(:mail_body_account_information_external, @user.auth_source.name) %>
+<% else %><%= l(:mail_body_account_information) %>:
+* <%= l(:field_login) %>: <%= @user.login %>
+* <%= l(:field_password) %>: <%= @password %>
+<% end %>
+<%= l(:label_login) %>: <%= @login_url %>
--- /dev/null
+<%= link_to @added_to, @added_to_url %><br />
+
+<ul><% @attachments.each do |attachment | %>
+<li><%= attachment.filename %></li>
+<% end %></ul>
--- /dev/null
+<%= @added_to %><% @attachments.each do |attachment | %>
+- <%= attachment.filename %><% end %>
+
+<%= @added_to_url %>
--- /dev/null
+<%= link_to @document.title, @document_url %> (<%= @document.category.name %>)<br />
+<br />
+<%= textilizable(@document, :description, :only_path => false) %>
--- /dev/null
+<%= @document.title %> (<%= @document.category.name %>)
+<%= @document_url %>
+
+<%= @document.description %>
--- /dev/null
+<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
+<hr />
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
--- /dev/null
+<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
+
+----------------------------------------
+<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
--- /dev/null
+<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
+
+<ul>
+<% for detail in @journal.details %>
+ <li><%= show_detail(detail, true) %></li>
+<% end %>
+</ul>
+
+<%= textilizable(@journal, :notes, :only_path => false) %>
+<hr />
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
--- /dev/null
+<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
+
+<% for detail in @journal.details -%>
+<%= show_detail(detail, true) %>
+<% end -%>
+
+<%= @journal.notes if @journal.notes? %>
+----------------------------------------
+<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
--- /dev/null
+<p><%= l(:mail_body_lost_password) %><br />
+<%= auto_link(@url) %></p>
+
+<p><%= l(:field_login) %>: <b><%= @token.user.login %></b></p>
--- /dev/null
+<%= l(:mail_body_lost_password) %>
+<%= @url %>
+
+<%= l(:field_login) %>: <%= @token.user.login %>
--- /dev/null
+<h1><%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to @message.subject, @message_url %></h1>
+<em><%= @message.author %></em>
+
+<%= textilizable(@message, :content, :only_path => false) %>
--- /dev/null
+<%= @message_url %>
+<%= @message.author %>
+
+<%= @message.content %>
--- /dev/null
+<h1><%= link_to @news.title, @news_url %></h1>
+<em><%= @news.author.name %></em>
+
+<%= textilizable(@news, :description, :only_path => false) %>
--- /dev/null
+<%= @news.title %>
+<%= @news_url %>
+<%= @news.author.name %>
+
+<%= @news.description %>
--- /dev/null
+<p><%= l(:mail_body_register) %><br />
+<%= auto_link(@url) %></p>
--- /dev/null
+<%= l(:mail_body_register) %>
+<%= @url %>
--- /dev/null
+<p><%= l(:mail_body_reminder, :count => @issues.size, :days => @days) %></p>
+
+<ul>
+<% @issues.each do |issue| -%>
+ <li><%=h issue.project %> - <%=link_to("#{issue.tracker} ##{issue.id}", :controller => 'issues', :action => 'show', :id => issue, :only_path => false)%>: <%=h issue.subject %></li>
+<% end -%>
+</ul>
+
+<p><%= link_to l(:label_issue_view_all), @issues_url %></p>
--- /dev/null
+<%= l(:mail_body_reminder, :count => @issues.size, :days => @days) %>:
+
+<% @issues.each do |issue| -%>
+* <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
+<% end -%>
+
+<%= @issues_url %>
--- /dev/null
+<p>This is a test email sent by Redmine.<br />
+Redmine URL: <%= auto_link(@url) %></p>
--- /dev/null
+This is a test email sent by Redmine.
+Redmine URL: <%= @url %>
--- /dev/null
+<p><%= l(:mail_body_wiki_content_added, :page => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url),
+ :author => h(@wiki_content.author)) %><br />
+<em><%=h @wiki_content.comments %></em></p>
--- /dev/null
+<%= l(:mail_body_wiki_content_added, :page => h(@wiki_content.page.pretty_title),
+ :author => h(@wiki_content.author)) %>
+<%= @wiki_content.comments %>
+
+<%= @wiki_content_url %>
--- /dev/null
+<p><%= l(:mail_body_wiki_content_updated, :page => link_to(h(@wiki_content.page.pretty_title), @wiki_content_url),
+ :author => h(@wiki_content.author)) %><br />
+<em><%=h @wiki_content.comments %></em></p>
+
+<p><%= l(:label_view_diff) %>:<br />
+<%= link_to @wiki_diff_url, @wiki_diff_url %></p>
--- /dev/null
+<%= l(:mail_body_wiki_content_updated, :page => h(@wiki_content.page.pretty_title),
+ :author => h(@wiki_content.author)) %>
+<%= @wiki_content.comments %>
+
+<%= @wiki_content.page.pretty_title %>:
+<%= @wiki_content_url %>
+<%= l(:label_view_diff) %>:
+<%= @wiki_diff_url %>
--- /dev/null
+<%= principals_check_box_tags 'member[user_ids][]', @principals %>
\ No newline at end of file
--- /dev/null
+<%= error_messages_for 'message' %>
+<% replying ||= false %>
+
+<div class="box">
+<!--[form:message]-->
+<p><label><%= l(:field_subject) %></label><br />
+<%= f.text_field :subject, :size => 120 %>
+
+<% if !replying && User.current.allowed_to?(:edit_messages, @project) %>
+ <label><%= f.check_box :sticky %> Sticky</label>
+ <label><%= f.check_box :locked %> Locked</label>
+<% end %>
+</p>
+
+<% if !replying && !@message.new_record? && User.current.allowed_to?(:edit_messages, @project) %>
+ <p><label><%= l(:label_board) %></label><br />
+ <%= f.select :board_id, @project.boards.collect {|b| [b.name, b.id]} %></p>
+<% end %>
+
+<p><%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p>
+<%= wikitoolbar_for 'message_content' %>
+<!--[eoform:message]-->
+
+<p><%= l(:label_attachment_plural) %><br />
+<%= render :partial => 'attachments/form' %></p>
+</div>
--- /dev/null
+<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @message.subject %></h2>
+
+<% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f, :replying => !@message.parent.nil?} %>
+ <%= submit_tag l(:button_save) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('message-form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+<div id="preview" class="wiki"></div>
--- /dev/null
+<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %></h2>
+
+<% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= submit_tag l(:button_create) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('message-form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+
+<div id="preview" class="wiki"></div>
--- /dev/null
+<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
+ link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
+
+<div class="contextual">
+ <%= watcher_tag(@topic, User.current) %>
+ <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
+ <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
+ <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
+</div>
+
+<h2><%=h @topic.subject %></h2>
+
+<div class="message">
+<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
+<div class="wiki">
+<%= textilizable(@topic.content, :attachments => @topic.attachments) %>
+</div>
+<%= link_to_attachments @topic, :author => false %>
+</div>
+<br />
+
+<% unless @replies.empty? %>
+<h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
+<% @replies.each do |message| %>
+ <div class="message reply" id="<%= "message-#{message.id}" %>">
+ <div class="contextual">
+ <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
+ <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
+ <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
+ </div>
+ <h4>
+ <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :anchor => "message-#{message.id}" } %>
+ -
+ <%= authoring message.created_on, message.author %>
+ </h4>
+ <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
+ <%= link_to_attachments message, :author => false %>
+ </div>
+<% end %>
+<% end %>
+
+<% if !@topic.locked? && authorize_for('messages', 'reply') %>
+<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
+<div id="reply" style="display:none;">
+<% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
+ <%= submit_tag l(:button_submit) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('message-form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+<div id="preview" class="wiki"></div>
+</div>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
+
+<% html_title h(@topic.subject) %>
--- /dev/null
+<div id="block_<%= block_name.dasherize %>" class="mypage-box">
+
+ <div style="float:right;margin-right:16px;z-index:500;">
+ <%= link_to_remote "", {
+ :url => { :action => "remove_block", :block => block_name },
+ :complete => "removeBlock('block_#{block_name.dasherize}')" },
+ :class => "close-icon"
+ %>
+ </div>
+
+ <div class="handle">
+ <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<h3><%=l(:label_my_account)%></h3>
+
+<p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br />
+<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p>
+<% if @user.rss_token %>
+<p><%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
+(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)</p>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %>
+<%= call_hook(:view_my_account_contextual, :user => @user)%>
+</div>
+<h2><%=l(:label_my_account)%></h2>
+<%= error_messages_for 'user' %>
+
+<% form_for :user, @user, :url => { :action => "account" },
+ :builder => TabularFormBuilder,
+ :lang => current_language,
+ :html => { :id => 'my_account_form' } do |f| %>
+<div class="splitcontentleft">
+<h3><%=l(:label_information_plural)%></h3>
+<div class="box tabular">
+<p><%= f.text_field :firstname, :required => true %></p>
+<p><%= f.text_field :lastname, :required => true %></p>
+<p><%= f.text_field :mail, :required => true %></p>
+<p><%= f.select :language, lang_options_for_select %></p>
+<% if Setting.openid? %>
+<p><%= f.text_field :identity_url %></p>
+<% end %>
+
+<% @user.custom_field_values.select(&:editable?).each do |value| %>
+ <p><%= custom_field_tag_with_label :user, value %></p>
+<% end %>
+<%= call_hook(:view_my_account, :user => @user, :form => f) %>
+</div>
+
+<%= submit_tag l(:button_save) %>
+</div>
+
+<div class="splitcontentright">
+<h3><%=l(:field_mail_notification)%></h3>
+<div class="box">
+<%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option),
+ :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %>
+<% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>
+<p><% User.current.projects.each do |project| %>
+ <label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br />
+<% end %></p>
+<p><em><%= l(:text_user_mail_option) %></em></p>
+<% end %>
+<p><label><%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %> <%= l(:label_user_mail_no_self_notified) %></label></p>
+</div>
+
+<h3><%=l(:label_preferences)%></h3>
+<div class="box tabular">
+<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %>
+<p><%= pref_fields.check_box :hide_mail %></p>
+<p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
+<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
+<% end %>
+</div>
+</div>
+<% end %>
+
+<% content_for :sidebar do %>
+<%= render :partial => 'sidebar' %>
+<% end %>
+
+<% html_title(l(:label_my_account)) -%>
--- /dev/null
+<h3><%= l(:label_calendar) %></h3>
+
+<% calendar = Redmine::Helpers::Calendar.new(Date.today, current_language, :week)
+ calendar.events = Issue.find :all,
+ :conditions => ["#{Issue.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')}) AND ((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", calendar.startdt, calendar.enddt, calendar.startdt, calendar.enddt],
+ :include => [:project, :tracker, :priority, :assigned_to] unless @user.projects.empty? %>
+
+<%= render :partial => 'common/calendar', :locals => {:calendar => calendar } %>
--- /dev/null
+<h3><%=l(:label_document_plural)%></h3>
+
+<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %>
+<%= render(:partial => 'documents/document',
+ :collection => Document.find(:all,
+ :limit => 10,
+ :order => "#{Document.table_name}.created_on DESC",
+ :conditions => "#{Document.table_name}.project_id in (#{project_ids.join(',')})",
+ :include => [:project])) unless project_ids.empty? %>
\ No newline at end of file
--- /dev/null
+<h3><%=l(:label_assigned_to_me_issues)%> (<%= Issue.visible.open.count(:conditions => {:assigned_to_id => User.current.id})%>)</h3>
+
+<% assigned_issues = Issue.visible.open.find(:all,
+ :conditions => {:assigned_to_id => User.current.id},
+ :limit => 10,
+ :include => [ :status, :project, :tracker, :priority ],
+ :order => "#{IssuePriority.table_name}.position DESC, #{Issue.table_name}.updated_on DESC") %>
+<%= render :partial => 'issues/list_simple', :locals => { :issues => assigned_issues } %>
+<% if assigned_issues.length > 0 %>
+<p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues',
+ :action => 'index',
+ :set_filter => 1,
+ :assigned_to_id => 'me',
+ :sort => 'priority:desc,updated_on:desc' %></p>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom,
+ {:controller => 'issues', :action => 'index', :set_filter => 1,
+ :assigned_to_id => 'me', :format => 'atom', :key => User.current.rss_key},
+ {:title => l(:label_assigned_to_me_issues)}) %>
+<% end %>
--- /dev/null
+<h3><%=l(:label_reported_issues)%> (<%= Issue.visible.count(:conditions => { :author_id => User.current.id }) %>)</h3>
+
+<% reported_issues = Issue.visible.find(:all,
+ :conditions => { :author_id => User.current.id },
+ :limit => 10,
+ :include => [ :status, :project, :tracker ],
+ :order => "#{Issue.table_name}.updated_on DESC") %>
+<%= render :partial => 'issues/list_simple', :locals => { :issues => reported_issues } %>
+<% if reported_issues.length > 0 %>
+<p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues',
+ :action => 'index',
+ :set_filter => 1,
+ :status_id => '*',
+ :author_id => 'me',
+ :sort => 'updated_on:desc' %></p>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom,
+ {:controller => 'issues', :action => 'index', :set_filter => 1,
+ :author_id => 'me', :format => 'atom', :key => User.current.rss_key},
+ {:title => l(:label_reported_issues)}) %>
+<% end %>
--- /dev/null
+<h3><%=l(:label_watched_issues)%> (<%= Issue.visible.count(:include => :watchers,
+ :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]) %>)</h3>
+<% watched_issues = Issue.visible.find(:all,
+ :include => [:status, :project, :tracker, :watchers],
+ :limit => 10,
+ :conditions => ["#{Watcher.table_name}.user_id = ?", user.id],
+ :order => "#{Issue.table_name}.updated_on DESC") %>
+
+<%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %>
+<% if watched_issues.length > 0 %>
+<p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues',
+ :action => 'index',
+ :set_filter => 1,
+ :watcher_id => 'me',
+ :sort => 'updated_on:desc' %></p>
+<% end %>
--- /dev/null
+<h3><%=l(:label_news_latest)%></h3>
+
+<%= render(:partial => 'news/news',
+ :collection => News.find(:all,
+ :limit => 10,
+ :order => "#{News.table_name}.created_on DESC",
+ :conditions => "#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})",
+ :include => [:project, :author])) unless @user.projects.empty? %>
\ No newline at end of file
--- /dev/null
+<h3><%=l(:label_spent_time)%> (<%= l(:label_last_n_days, 7) %>)</h3>
+<%
+entries = TimeEntry.find(:all,
+ :conditions => ["#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", @user.id, Date.today - 6, Date.today],
+ :include => [:activity, :project, {:issue => [:tracker, :status]}],
+ :order => "#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC")
+entries_by_day = entries.group_by(&:spent_on)
+%>
+
+<div class="total-hours">
+<p><%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %></p>
+</div>
+
+<% if entries.any? %>
+<table class="list time-entries">
+<thead>
+<th><%= l(:label_activity) %></th>
+<th><%= l(:label_project) %></th>
+<th><%= l(:field_comments) %></th>
+<th><%= l(:field_hours) %></th>
+<th></th>
+</thead>
+<tbody>
+<% entries_by_day.keys.sort.reverse.each do |day| %>
+ <tr class="odd">
+ <td><strong><%= day == Date.today ? l(:label_today).titleize : format_date(day) %></strong></td>
+ <td colspan="2"></td>
+ <td class="hours"><em><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %></em></td>
+ <td></td>
+ </tr>
+ <% entries_by_day[day].each do |entry| -%>
+ <tr class="time-entry" style="border-bottom: 1px solid #f5f5f5;">
+ <td class="activity"><%=h entry.activity %></td>
+ <td class="subject"><%=h entry.project %> <%= ' - ' + link_to_issue(entry.issue, :truncate => 50) if entry.issue %></td>
+ <td class="comments"><%=h entry.comments %></td>
+ <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
+ <td align="center">
+ <% if entry.editable_by?(@user) -%>
+ <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry},
+ :title => l(:button_edit) %>
+ <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry},
+ :confirm => l(:text_are_you_sure),
+ :method => :post,
+ :title => l(:button_delete) %>
+ <% end -%>
+ </td>
+ </tr>
+ <% end -%>
+<% end -%>
+</tbody>
+</table>
+<% end %>
--- /dev/null
+<div class="contextual">
+ <%= link_to l(:label_personalize_page), :action => 'page_layout' %>
+</div>
+
+<h2><%=l(:label_my_page)%></h2>
+
+<div id="list-top">
+ <% @blocks['top'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <div class="mypage-box">
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
+ </div>
+ <% end if @blocks['top'] %>
+</div>
+
+<div id="list-left" class="splitcontentleft">
+ <% @blocks['left'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <div class="mypage-box">
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
+ </div>
+ <% end if @blocks['left'] %>
+</div>
+
+<div id="list-right" class="splitcontentright">
+ <% @blocks['right'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <div class="mypage-box">
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %>
+ </div>
+ <% end if @blocks['right'] %>
+</div>
+
+<% content_for :header_tags do %>
+ <%= javascript_include_tag 'context_menu' %>
+ <%= stylesheet_link_tag 'context_menu' %>
+<% end %>
+
+<div id="context-menu" style="display: none;"></div>
+<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
+
+<% html_title(l(:label_my_page)) -%>
--- /dev/null
+<script language="JavaScript">
+//<![CDATA[
+function recreateSortables() {
+ Sortable.destroy('list-top');
+ Sortable.destroy('list-left');
+ Sortable.destroy('list-right');
+
+ Sortable.create("list-top", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=top', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-top")})}, only:'mypage-box', tag:'div'})
+ Sortable.create("list-left", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=left', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-left")})}, only:'mypage-box', tag:'div'})
+ Sortable.create("list-right", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=right', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-right")})}, only:'mypage-box', tag:'div'})
+}
+
+function updateSelect() {
+ s = $('block-select')
+ for (var i = 0; i < s.options.length; i++) {
+ if ($('block_' + s.options[i].value)) {
+ s.options[i].disabled = true;
+ } else {
+ s.options[i].disabled = false;
+ }
+ }
+ s.options[0].selected = true;
+}
+
+function afterAddBlock() {
+ recreateSortables();
+ updateSelect();
+}
+
+function removeBlock(block) {
+ Effect.DropOut(block);
+ updateSelect();
+}
+//]]>
+</script>
+
+<div class="contextual">
+<% form_tag({:action => "add_block"}, :id => "block-form") do %>
+<%= select_tag 'block', "<option></option>" + options_for_select(@block_options), :id => "block-select" %>
+<%= link_to_remote l(:button_add),
+ {:url => { :action => "add_block" },
+ :with => "Form.serialize('block-form')",
+ :update => "list-top",
+ :position => :top,
+ :complete => "afterAddBlock();"
+ }, :class => 'icon icon-add'
+ %>
+<% end %>
+<%= link_to l(:button_save), {:action => 'page_layout_save'}, :class => 'icon icon-save' %>
+<%= link_to l(:button_cancel), {:action => 'page'}, :class => 'icon icon-cancel' %>
+</div>
+
+<h2><%=l(:label_my_page)%></h2>
+
+<div id="list-top" class="block-receiver">
+ <% @blocks['top'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
+ <% end if @blocks['top'] %>
+</div>
+
+<div id="list-left" class="splitcontentleft block-receiver">
+ <% @blocks['left'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
+ <% end if @blocks['left'] %>
+</div>
+
+<div id="list-right" class="splitcontentright block-receiver">
+ <% @blocks['right'].each do |b|
+ next unless MyController::BLOCKS.keys.include? b %>
+ <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
+ <% end if @blocks['right'] %>
+</div>
+
+<%= sortable_element 'list-top',
+ :tag => 'div',
+ :only => 'mypage-box',
+ :handle => "handle",
+ :dropOnEmpty => true,
+ :containment => ['list-top', 'list-left', 'list-right'],
+ :constraint => false,
+ :url => { :action => "order_blocks", :group => "top" }
+ %>
+
+
+<%= sortable_element 'list-left',
+ :tag => 'div',
+ :only => 'mypage-box',
+ :handle => "handle",
+ :dropOnEmpty => true,
+ :containment => ['list-top', 'list-left', 'list-right'],
+ :constraint => false,
+ :url => { :action => "order_blocks", :group => "left" }
+ %>
+
+<%= sortable_element 'list-right',
+ :tag => 'div',
+ :only => 'mypage-box',
+ :handle => "handle",
+ :dropOnEmpty => true,
+ :containment => ['list-top', 'list-left', 'list-right'],
+ :constraint => false,
+ :url => { :action => "order_blocks", :group => "right" }
+ %>
+
+<%= javascript_tag "updateSelect()" %>
--- /dev/null
+<h2><%=l(:button_change_password)%></h2>
+
+<%= error_messages_for 'user' %>
+
+<% form_tag({}, :class => "tabular") do %>
+<div class="box">
+<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
+<%= password_field_tag 'password', nil, :size => 25 %></p>
+
+<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
+<%= password_field_tag 'new_password', nil, :size => 25 %><br />
+<em><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
+
+<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
+<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
+</div>
+<%= submit_tag l(:button_apply) %>
+<% end %>
+
+<% content_for :sidebar do %>
+<%= render :partial => 'sidebar' %>
+<% end %>
--- /dev/null
+<%= error_messages_for 'news' %>
+<div class="box tabular">
+<p><%= f.text_field :title, :required => true, :size => 60 %></p>
+<p><%= f.text_area :summary, :cols => 60, :rows => 2 %></p>
+<p><%= f.text_area :description, :required => true, :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
+</div>
+
+<%= wikitoolbar_for 'news_description' %>
--- /dev/null
+<p><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %>
+<%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
+<%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %>
+<br />
+<% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %>
+<span class="author"><%= authoring news.created_on, news.author %></span></p>
--- /dev/null
+<h2><%=l(:label_news)%></h2>
+
+<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" },
+ :html => { :id => 'news-form' } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('news-form')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+<div id="preview" class="wiki"></div>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized(l(:label_news_new),
+ {:controller => 'news', :action => 'new', :project_id => @project},
+ :class => 'icon icon-add',
+ :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project %>
+</div>
+
+<div id="add-news" style="display:none;">
+<h2><%=l(:label_news_new)%></h2>
+<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project },
+ :html => { :id => 'news-form' } do |f| %>
+<%= render :partial => 'news/form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('news-form')"
+ }, :accesskey => accesskey(:preview) %> |
+<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
+<% end if @project %>
+<div id="preview" class="wiki"></div>
+</div>
+
+<h2><%=l(:label_news_plural)%></h2>
+
+<% if @newss.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% else %>
+<% @newss.each do |news| %>
+ <h3><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %>
+ <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
+ <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
+ <p class="author"><%= authoring news.created_on, news.author %></p>
+ <div class="wiki">
+ <%= textilizable(news.description) %>
+ </div>
+<% end %>
+<% end %>
+<p class="pagination"><%= pagination_links_full @news_pages %></p>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
+<% end %>
+
+<% html_title(l(:label_news_plural)) -%>
--- /dev/null
+<h2><%=l(:label_news_new)%></h2>
+
+<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project },
+ :html => { :id => 'news-form' } do |f| %>
+<%= render :partial => 'news/form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('news-form')"
+ }, :accesskey => accesskey(:preview) %>
+<% end %>
+<div id="preview" class="wiki"></div>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:button_edit),
+ {:controller => 'news', :action => 'edit', :id => @news},
+ :class => 'icon icon-edit',
+ :accesskey => accesskey(:edit),
+ :onclick => 'Element.show("edit-news"); return false;' %>
+<%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy', :id => @news}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+</div>
+
+<h2><%=h @news.title %></h2>
+
+<% if authorize_for('news', 'edit') %>
+<div id="edit-news" style="display:none;">
+<% labelled_tabular_form_for :news, @news, :url => { :action => "edit", :id => @news },
+ :html => { :id => 'news-form' } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('news-form')"
+ }, :accesskey => accesskey(:preview) %> |
+<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("edit-news"); return false;' %>
+<% end %>
+<div id="preview" class="wiki"></div>
+</div>
+<% end %>
+
+<p><em><% unless @news.summary.blank? %><%=h @news.summary %><br /><% end %>
+<span class="author"><%= authoring @news.created_on, @news.author %></span></em></p>
+<div class="wiki">
+<%= textilizable(@news.description) %>
+</div>
+<br />
+
+<div id="comments" style="margin-bottom:16px;">
+<h3 class="icon22 icon22-comment"><%= l(:label_comment_plural) %></h3>
+<% @comments.each do |comment| %>
+ <% next if comment.new_record? %>
+ <div class="contextual">
+ <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment},
+ :confirm => l(:text_are_you_sure), :method => :post, :title => l(:button_delete) %>
+ </div>
+ <h4><%= authoring comment.created_on, comment.author %></h4>
+ <%= textilizable(comment.comments) %>
+<% end if @comments.any? %>
+</div>
+
+<% if authorize_for 'news', 'add_comment' %>
+<p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
+<% form_tag({:action => 'add_comment', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
+<div class="box">
+ <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
+ <%= wikitoolbar_for 'comment_comments' %>
+</div>
+<p><%= submit_tag l(:button_add) %></p>
+<% end %>
+<% end %>
+
+<% html_title @news.title -%>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
--- /dev/null
+<% labelled_tabular_form_for :project, @project, :url => { :action => "edit", :id => @project } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<%= error_messages_for 'project' %>
+
+<div class="box">
+<!--[form:project]-->
+<p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
+
+<% unless @project.allowed_parents.empty? %>
+ <p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
+<% end %>
+
+<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
+<p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
+<% unless @project.identifier_frozen? %>
+<br /><em><%= l(:text_length_between, :min => 1, :max => 20) %> <%= l(:text_project_identifier_info) %></em>
+<% end %></p>
+<p><%= f.text_field :homepage, :size => 60 %></p>
+<p><%= f.check_box :is_public %></p>
+<%= wikitoolbar_for 'project_description' %>
+
+<% @project.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :project, value %></p>
+<% end %>
+<%= call_hook(:view_projects_form, :project => @project, :form => f) %>
+</div>
+
+<% unless @trackers.empty? %>
+<fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
+<% @trackers.each do |tracker| %>
+ <label class="floating">
+ <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
+ <%= tracker %>
+ </label>
+<% end %>
+<%= hidden_field_tag 'project[tracker_ids][]', '' %>
+</fieldset>
+<% end %>
+
+<% unless @issue_custom_fields.empty? %>
+<fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
+<% @issue_custom_fields.each do |custom_field| %>
+ <label class="floating">
+ <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
+ <%= custom_field.name %>
+ </label>
+<% end %>
+<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
+</fieldset>
+<% end %>
+<!--[eoform:project]-->
--- /dev/null
+<h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
+<p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
+
+<div id="activity">
+<% @events_by_day.keys.sort.reverse.each do |day| %>
+<h3><%= format_activity_day(day) %></h3>
+<dl>
+<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
+ <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
+ <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
+ <span class="time"><%= format_time(e.event_datetime, false) %></span>
+ <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
+ <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
+ <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
+ <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
+<% end -%>
+</dl>
+<% end -%>
+</div>
+
+<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
+
+<div style="float:left;">
+<%= link_to_remote(('« ' + l(:label_previous)),
+ {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
+ {:href => url_for(params.merge(:from => @date_to - @days - 1)),
+ :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))}) %>
+</div>
+<div style="float:right;">
+<%= link_to_remote((l(:label_next) + ' »'),
+ {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
+ {:href => url_for(params.merge(:from => @date_to + @days - 1)),
+ :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))}) unless @date_to >= Date.today %>
+</div>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
+<% end %>
+
+<% content_for :sidebar do %>
+<% form_tag({}, :method => :get) do %>
+<h3><%= l(:label_activity) %></h3>
+<p><% @activity.event_types.each do |t| %>
+<%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
+<%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%>
+<br />
+<% end %></p>
+<% if @project && @project.descendants.active.any? %>
+ <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
+ <%= hidden_field_tag 'with_subprojects', 0 %>
+<% end %>
+<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
+<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
+<% end %>
+<% end %>
+
+<% html_title(l(:label_activity), @author) -%>
--- /dev/null
+<h2><%=l(:label_project_new)%></h2>
+
+<% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+
+<fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
+<% Redmine::AccessControl.available_project_modules.each do |m| %>
+ <label class="floating">
+ <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
+ <%= l_or_humanize(m, :prefix => "project_module_") %>
+ </label>
+<% end %>
+</fieldset>
+
+<%= submit_tag l(:button_save) %>
+<%= javascript_tag "Form.Element.focus('project_name');" %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_attachment_new)%></h2>
+
+<%= error_messages_for 'attachment' %>
+<div class="box">
+<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %>
+
+<% if @versions.any? %>
+<p><label for="version_id"><%=l(:field_version)%></label>
+<%= select_tag "version_id", content_tag('option', '') +
+ options_from_collection_for_select(@versions, "id", "name") %></p>
+<% end %>
+
+<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
+</div>
+<%= submit_tag l(:button_add) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_issue_category_new)%></h2>
+
+<% labelled_tabular_form_for :category, @category, :url => { :action => 'add_issue_category' } do |f| %>
+<%= render :partial => 'issue_categories/form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_version_new)%></h2>
+
+<% labelled_tabular_form_for :version, @version, :url => { :action => 'add_version' } do |f| %>
+<%= render :partial => 'versions/form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
\ No newline at end of file
--- /dev/null
+<h2><%=l(:label_change_log)%></h2>
+
+<% if @versions.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<% @versions.each do |version| %>
+ <%= tag 'a', :name => version.name %>
+ <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
+ <% if version.effective_date %>
+ <p><%= format_date(version.effective_date) %></p>
+ <% end %>
+ <p><%=h version.description %></p>
+ <% issues = version.fixed_issues.find(:all,
+ :include => [:status, :tracker, :priority],
+ :conditions => ["#{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", true],
+ :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty?
+ issues ||= []
+ %>
+ <% if !issues.empty? %>
+ <ul>
+ <% issues.each do |issue| %>
+ <li><%= link_to_issue(issue) %></li>
+ <% end %>
+ </ul>
+ <% end %>
+<% end %>
+
+<% content_for :sidebar do %>
+<% form_tag({},:method => :get) do %>
+<h3><%= l(:label_change_log) %></h3>
+<% @trackers.each do |tracker| %>
+ <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
+ <%= tracker.name %></label><br />
+<% end %>
+<p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
+<% end %>
+
+<h3><%= l(:label_version_plural) %></h3>
+<% @versions.each do |version| %>
+<%= link_to version.name, :anchor => version.name %><br />
+<% end %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_project_new)%></h2>
+
+<% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+
+<fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
+<% Redmine::AccessControl.available_project_modules.each do |m| %>
+ <label class="floating">
+ <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
+ <%= l_or_humanize(m, :prefix => "project_module_") %>
+ </label>
+<% end %>
+</fieldset>
+
+<fieldset class="box"><legend><%= l(:button_copy) %></legend>
+ <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
+ <label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label>
+ <%= hidden_field_tag 'only[]', '' %>
+</fieldset>
+
+<%= submit_tag l(:button_copy) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_confirmation)%></h2>
+<div class="warning">
+<p><strong><%=h @project_to_destroy %></strong><br />
+<%=l(:text_project_destroy_confirmation)%>
+
+<% if @project_to_destroy.descendants.any? %>
+<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
+<% end %>
+</p>
+<p>
+ <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %>
+ <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
+ <%= submit_tag l(:button_delete) %>
+ <% end %>
+</p>
+</div>
--- /dev/null
+<div class="contextual">
+ <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
+ <%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
+ <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%>
+</div>
+
+<h2><%=l(:label_project_plural)%></h2>
+
+<%= render_project_hierarchy(@projects)%>
+
+<% if User.current.logged? %>
+<p style="text-align:right;">
+<span class="my-project"><%= l(:label_my_projects) %></span>
+</p>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
+<% end %>
+
+<% html_title(l(:label_project_plural)) -%>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_attachment_plural)%></h2>
+
+<% delete_allowed = User.current.allowed_to?(:manage_files, @project) %>
+
+<table class="list files">
+ <thead><tr>
+ <%= sort_header_tag('filename', :caption => l(:field_filename)) %>
+ <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %>
+ <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %>
+ <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
+ <th>MD5</th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% @containers.each do |container| %>
+ <% next if container.attachments.empty? -%>
+ <% if container.is_a?(Version) -%>
+ <tr>
+ <th colspan="6" align="left">
+ <%= link_to(h(container), {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %>
+ </th>
+ </tr>
+ <% end -%>
+ <% container.attachments.each do |file| %>
+ <tr class="file <%= cycle("odd", "even") %>">
+ <td class="filename"><%= link_to_attachment file, :download => true, :title => file.description %></td>
+ <td class="created_on"><%= format_time(file.created_on) %></td>
+ <td class="filesize"><%= number_to_human_size(file.filesize) %></td>
+ <td class="downloads"><%= file.downloads %></td>
+ <td class="digest"><%= file.digest %></td>
+ <td align="center">
+ <%= link_to(image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
+ :confirm => l(:text_are_you_sure), :method => :post) if delete_allowed %>
+ </td>
+ </tr>
+ <% end
+ reset_cycle %>
+<% end %>
+ </tbody>
+</table>
+
+<% html_title(l(:label_attachment_plural)) -%>
--- /dev/null
+<h2><%=l(:label_member_plural)%></h2>
+
+<% if @members.empty? %><p><i><%= l(:label_no_data) %></i></p><% end %>
+
+<% members = @members.group_by {|m| m.role } %>
+<% members.keys.sort{|x,y| x.position <=> y.position}.each do |role| %>
+<h3><%= role.name %></h3>
+<ul>
+<% members[role].each do |m| %>
+<li><%= link_to_user m.user %> (<%= format_date m.created_on %>)</li>
+<% end %>
+</ul>
+<% end %>
--- /dev/null
+<h2><%=l(:label_roadmap)%></h2>
+
+<% if @versions.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% else %>
+<div id="roadmap">
+<% @versions.each do |version| %>
+ <%= tag 'a', :name => version.name %>
+ <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
+ <%= render :partial => 'versions/overview', :locals => {:version => version} %>
+ <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
+
+ <% issues = version.fixed_issues.find(:all,
+ :include => [:status, :tracker, :priority],
+ :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"],
+ :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty?
+ issues ||= []
+ %>
+ <% if issues.size > 0 %>
+ <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
+ <ul>
+ <%- issues.each do |issue| -%>
+ <li><%= link_to_issue(issue) %></li>
+ <%- end -%>
+ </ul>
+ </fieldset>
+ <% end %>
+ <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
+<% end %>
+</div>
+<% end %>
+
+<% content_for :sidebar do %>
+<% form_tag({}, :method => :get) do %>
+<h3><%= l(:label_roadmap) %></h3>
+<% @trackers.each do |tracker| %>
+ <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
+ <%= tracker.name %></label><br />
+<% end %>
+<br />
+<label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
+<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
+<% end %>
+
+<h3><%= l(:label_version_plural) %></h3>
+<% @versions.each do |version| %>
+<%= link_to version.name, "##{version.name}" %><br />
+<% end %>
+<% end %>
+
+<% html_title(l(:label_roadmap)) %>
--- /dev/null
+<h2><%=l(:label_settings)%></h2>
+
+<%= render_tabs project_settings_tabs %>
+
+<% html_title(l(:label_settings)) -%>
--- /dev/null
+<% form_tag({:controller => 'projects', :action => 'save_activities', :id => @project}, :class => "tabular") do %>
+
+<table class="list">
+ <tr>
+ <th><%= l(:field_name) %></th>
+ <th><%= l(:enumeration_system_activity) %></th>
+ <% TimeEntryActivity.new.available_custom_fields.each do |value| %>
+ <th><%= h value.name %></th>
+ <% end %>
+ <th style="width:15%;"><%= l(:field_active) %></th>
+ </tr>
+
+ <% @project.activities(true).each do |enumeration| %>
+ <% fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
+ <tr class="<%= cycle('odd', 'even') %>">
+ <td>
+ <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
+ <%= h(enumeration) %>
+ </td>
+ <td align="center" style="width:15%;"><%= image_tag('true.png') unless enumeration.project %></td>
+ <% enumeration.custom_field_values.each do |value| %>
+ <td align="center">
+ <%= custom_field_tag "enumerations[#{enumeration.id}]", value %>
+ </td>
+ <% end %>
+ <td align="center" style="width:15%;">
+ <%= ff.check_box :active %>
+ </td>
+ </tr>
+ <% end %>
+ <% end %>
+</table>
+
+<div class="contextual">
+<%= link_to(l(:button_reset), {:controller => 'projects', :action => 'reset_activities', :id => @project},
+ :method => :delete,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del') %>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% if @project.boards.any? %>
+<table class="list">
+ <thead><th><%= l(:label_board) %></th><th><%= l(:field_description) %></th><th style="width:15%"></th><th style="width:15%"></th><th style="width:15%"></th></thead>
+ <tbody>
+<% @project.boards.each do |board|
+ next if board.new_record? %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%=h board.name %></td>
+ <td><%=h board.description %></td>
+ <td align="center">
+ <% if authorize_for("boards", "edit") %>
+ <%= reorder_links('board', {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board}) %>
+ <% end %>
+ </td>
+ <td align="center"><%= link_to_if_authorized l(:button_edit), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board}, :class => 'icon icon-edit' %></td>
+ <td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'boards', :action => 'destroy', :project_id => @project, :id => board}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %></td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<p><%= link_to_if_authorized l(:label_board_new), {:controller => 'boards', :action => 'new', :project_id => @project} %></p>
--- /dev/null
+<% if @project.issue_categories.any? %>
+<table class="list">
+ <thead>
+ <th><%= l(:label_issue_category) %></th>
+ <th><%= l(:field_assigned_to) %></th>
+ <th style="width:15%"></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+<% for category in @project.issue_categories %>
+ <% unless category.new_record? %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%=h(category.name) %></td>
+ <td><%=h(category.assigned_to.name) if category.assigned_to %></td>
+ <td align="center"><%= link_to_if_authorized l(:button_edit), { :controller => 'issue_categories', :action => 'edit', :id => category }, :class => 'icon icon-edit' %></td>
+ <td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => category}, :method => :post, :class => 'icon icon-del' %></td>
+ </tr>
+ <% end %>
+<% end %>
+ </tbody>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<p><%= link_to_if_authorized l(:label_issue_category_new), :controller => 'projects', :action => 'add_issue_category', :id => @project %></p>
--- /dev/null
+<%= error_messages_for 'member' %>
+<% roles = Role.find_all_givable
+ members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
+
+<div class="splitcontentleft">
+<% if members.any? %>
+<table class="list members">
+ <thead>
+ <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
+ <th><%= l(:label_role_plural) %></th>
+ <th style="width:15%"></th>
+ <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
+ </thead>
+ <tbody>
+ <% members.each do |member| %>
+ <% next if member.new_record? %>
+ <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
+ <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
+ <td class="roles">
+ <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
+ <% if authorize_for('members', 'edit') %>
+ <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member},
+ :method => :post,
+ :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
+ <p><% roles.each do |role| %>
+ <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
+ :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
+ <% end %></p>
+ <%= hidden_field_tag 'member[role_ids][]', '' %>
+ <p><%= submit_tag l(:button_change), :class => "small" %>
+ <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
+ <% end %>
+ <% end %>
+ </td>
+ <td class="buttons">
+ <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
+ <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
+ :method => :post
+ }, :title => l(:button_delete),
+ :class => 'icon icon-del') if member.deletable? %>
+ </td>
+ <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
+ </tr>
+ </tbody>
+<% end; reset_cycle %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+
+<% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
+
+<div class="splitcontentright">
+<% if roles.any? && principals.any? %>
+ <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
+ <fieldset><legend><%=l(:label_member_new)%></legend>
+
+ <p><%= text_field_tag 'principal_search', nil, :size => "40" %></p>
+ <%= observe_field(:principal_search,
+ :frequency => 0.5,
+ :update => :principals,
+ :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project },
+ :with => 'q')
+ %>
+
+ <div id="principals">
+ <%= principals_check_box_tags 'member[user_ids][]', principals %>
+ </div>
+
+ <p><%= l(:label_role_plural) %>:
+ <% roles.each do |role| %>
+ <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
+ <% end %></p>
+
+ <p><%= submit_tag l(:button_add) %></p>
+ </fieldset>
+ <% end %>
+<% end %>
+</div>
--- /dev/null
+<% form_for :project, @project,
+ :url => { :action => 'modules', :id => @project },
+ :html => {:id => 'modules-form'} do |f| %>
+
+<div class=box>
+<strong><%= l(:text_select_project_modules) %></strong>
+
+<% Redmine::AccessControl.available_project_modules.each do |m| %>
+<p><label><%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) -%>
+ <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
+<% end %>
+</div>
+
+<p><%= check_all_links 'modules-form' %></p>
+<p><%= submit_tag l(:button_save) %></p>
+
+<% end %>
--- /dev/null
+<% remote_form_for :repository, @repository,
+ :url => { :controller => 'repositories', :action => 'edit', :id => @project },
+ :builder => TabularFormBuilder,
+ :lang => current_language do |f| %>
+
+<%= error_messages_for 'repository' %>
+
+<div class="box tabular">
+<p><label><%= l(:label_scm) %></label><%= scm_select_tag(@repository) %></p>
+<%= repository_field_tags(f, @repository) if @repository %>
+</div>
+
+<div class="contextual">
+<% if @repository && !@repository.new_record? %>
+<%= link_to(l(:label_user_plural), {:controller => 'repositories', :action => 'committers', :id => @project}, :class => 'icon icon-user') %>
+<%= link_to(l(:button_delete), {:controller => 'repositories', :action => 'destroy', :id => @project},
+ :confirm => l(:text_are_you_sure),
+ :method => :post,
+ :class => 'icon icon-del') %>
+<% end %>
+</div>
+
+<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save), :disabled => @repository.nil?) %>
+<% end %>
--- /dev/null
+<% if @project.versions.any? %>
+<table class="list versions">
+ <thead>
+ <th><%= l(:label_version) %></th>
+ <th><%= l(:field_effective_date) %></th>
+ <th><%= l(:field_description) %></th>
+ <th><%= l(:field_status) %></th>
+ <th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+<% for version in @project.versions.sort %>
+ <tr class="version <%= cycle 'odd', 'even' %> <%=h version.status %>">
+ <td><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></td>
+ <td align="center"><%= format_date(version.effective_date) %></td>
+ <td><%=h version.description %></td>
+ <td><%= l("version_status_#{version.status}") %></td>
+ <td><%= link_to(h(version.wiki_page_title), :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td>
+ <td class="buttons">
+ <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %>
+ <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+<% end; reset_cycle %>
+ </tbody>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<div class="contextual">
+<% if @project.versions.any? %>
+ <%= link_to 'Close completed versions', {:controller => 'versions', :action => 'close_completed', :project_id => @project}, :method => :post %>
+<% end %>
+</div>
+
+<p><%= link_to_if_authorized l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %></p>
--- /dev/null
+<% remote_form_for :wiki, @wiki,
+ :url => { :controller => 'wikis', :action => 'edit', :id => @project },
+ :builder => TabularFormBuilder,
+ :lang => current_language do |f| %>
+
+<%= error_messages_for 'wiki' %>
+
+<div class="box tabular">
+<p><%= f.text_field :start_page, :size => 60, :required => true %><br />
+<em><%= l(:text_unallowed_characters) %>: , . / ? ; : |</em></p>
+</div>
+
+<div class="contextual">
+<%= link_to(l(:button_delete), {:controller => 'wikis', :action => 'destroy', :id => @project},
+ :class => 'icon icon-del') if @wiki && !@wiki.new_record? %>
+</div>
+
+<%= submit_tag((@wiki.nil? || @wiki.new_record?) ? l(:button_create) : l(:button_save)) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_overview)%></h2>
+
+<div class="splitcontentleft">
+ <%= textilizable @project.description %>
+ <ul>
+ <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
+ <% if @subprojects.any? %>
+ <li><%=l(:label_subproject_plural)%>:
+ <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
+ <% end %>
+ <% @project.custom_values.each do |custom_value| %>
+ <% if !custom_value.value.blank? %>
+ <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
+ <% end %>
+ <% end %>
+ </ul>
+
+ <% if User.current.allowed_to?(:view_issues, @project) %>
+ <div class="box">
+ <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
+ <ul>
+ <% for tracker in @trackers %>
+ <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
+ :set_filter => 1,
+ "tracker_id" => tracker.id %>:
+ <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
+ :total => @total_issues_by_tracker[tracker].to_i) %>
+ </li>
+ <% end %>
+ </ul>
+ <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
+ </div>
+ <% end %>
+ <%= call_hook(:view_projects_show_left, :project => @project) %>
+</div>
+
+<div class="splitcontentright">
+ <% if @users_by_role.any? %>
+ <div class="box">
+ <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
+ <p><% @users_by_role.keys.sort.each do |role| %>
+ <%=h role %>: <%= @users_by_role[role].sort.collect{|u| link_to_user u}.join(", ") %><br />
+ <% end %></p>
+ </div>
+ <% end %>
+
+ <% if @news.any? && authorize_for('news', 'index') %>
+ <div class="box">
+ <h3><%=l(:label_news_latest)%></h3>
+ <%= render :partial => 'news/news', :collection => @news %>
+ <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
+ </div>
+ <% end %>
+ <%= call_hook(:view_projects_show_right, :project => @project) %>
+</div>
+
+<% content_for :sidebar do %>
+ <% planning_links = []
+ planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
+ planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
+ planning_links.compact!
+ unless planning_links.empty? %>
+ <h3><%= l(:label_planning) %></h3>
+ <p><%= planning_links.join(' | ') %></p>
+ <% end %>
+
+ <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
+ <h3><%= l(:label_spent_time) %></h3>
+ <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
+ <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
+ <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
+ <% end %>
+ <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
+<% end %>
+
+<% html_title(l(:label_overview)) -%>
--- /dev/null
+<% content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
+<legend><%= l(:field_column_names) %></legend>
+
+<%= hidden_field_tag 'query[column_names][]', '', :id => nil %>
+<table>
+ <tr>
+ <td><%= select_tag 'available_columns',
+ options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}),
+ :multiple => true, :size => 10, :style => "width:150px" %>
+ </td>
+ <td align="center" valign="middle">
+ <input type="button" value="-->"
+ onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
+ <input type="button" value="<--"
+ onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
+ </td>
+ <td><%= select_tag 'query[column_names][]',
+ options_for_select(@query.columns.collect {|column| [column.caption, column.name]}),
+ :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px" %>
+ </td>
+ </tr>
+</table>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= javascript_include_tag 'select_list_move' %>
+<% end %>
--- /dev/null
+<script type="text/javascript">
+//<![CDATA[
+function add_filter() {
+ select = $('add_filter_select');
+ field = select.value
+ Element.show('tr_' + field);
+ check_box = $('cb_' + field);
+ check_box.checked = true;
+ toggle_filter(field);
+ select.selectedIndex = 0;
+
+ for (i=0; i<select.options.length; i++) {
+ if (select.options[i].value == field) {
+ select.options[i].disabled = true;
+ }
+ }
+}
+
+function toggle_filter(field) {
+ check_box = $('cb_' + field);
+
+ if (check_box.checked) {
+ Element.show("operators_" + field);
+ toggle_operator(field);
+ } else {
+ Element.hide("operators_" + field);
+ Element.hide("div_values_" + field);
+ }
+}
+
+function toggle_operator(field) {
+ operator = $("operators_" + field);
+ switch (operator.value) {
+ case "!*":
+ case "*":
+ case "t":
+ case "w":
+ case "o":
+ case "c":
+ Element.hide("div_values_" + field);
+ break;
+ default:
+ Element.show("div_values_" + field);
+ break;
+ }
+}
+
+function toggle_multi_select(field) {
+ select = $('values_' + field);
+ if (select.multiple == true) {
+ select.multiple = false;
+ } else {
+ select.multiple = true;
+ }
+}
+//]]>
+</script>
+
+<table width="100%">
+<tr>
+<td>
+<table>
+<% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
+ <% field = filter[0]
+ options = filter[1] %>
+ <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
+ <td style="width:200px;">
+ <%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
+ <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
+ </td>
+ <td style="width:150px;">
+ <%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %>
+ </td>
+ <td>
+ <div id="div_values_<%= field %>" style="display:none;">
+ <% case options[:type]
+ when :list, :list_optional, :list_status, :list_subprojects %>
+ <select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="values[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;">
+ <%= options_for_select options[:values], query.values_for(field) %>
+ </select>
+ <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
+ <% when :date, :date_past %>
+ <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
+ <% when :string, :text %>
+ <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %>
+ <% when :integer %>
+ <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %>
+ <% end %>
+ </div>
+ <script type="text/javascript">toggle_filter('<%= field %>');</script>
+ </td>
+ </tr>
+<% end %>
+</table>
+</td>
+<td class="add-filter">
+<%= l(:label_filter_add) %>:
+<%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact),
+ :onchange => "add_filter();",
+ :class => "select-small",
+ :name => nil %>
+</td>
+</tr>
+</table>
--- /dev/null
+<%= error_messages_for 'query' %>
+<%= hidden_field_tag 'confirm', 1 %>
+
+<div class="box">
+<div class="tabular">
+<p><label for="query_name"><%=l(:field_name)%></label>
+<%= text_field 'query', 'name', :size => 80 %></p>
+
+<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
+<p><label for="query_is_public"><%=l(:field_is_public)%></label>
+<%= check_box 'query', 'is_public',
+ :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
+<% end %>
+
+<p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
+<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
+ :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
+
+<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
+<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
+ :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
+
+<p><label for="query_group_by"><%= l(:field_group_by) %></label>
+<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
+</div>
+
+<fieldset><legend><%= l(:label_filter_plural) %></legend>
+<%= render :partial => 'queries/filters', :locals => {:query => query}%>
+</fieldset>
+
+<fieldset><legend><%= l(:label_sort) %></legend>
+<% 3.times do |i| %>
+<%= i+1 %>: <%= 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))) %>
+ <%= select_tag("query[sort_criteria][#{i}][]",
+ options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i))) %><br />
+<% end %>
+</fieldset>
+
+<%= render :partial => 'queries/columns', :locals => {:query => query}%>
+</div>
--- /dev/null
+<h2><%= l(:label_query) %></h2>
+
+<% form_tag({:action => 'edit', :id => @query}, :onsubmit => 'selectAllOptions("selected_columns");') do %>
+ <%= render :partial => 'form', :locals => {:query => @query} %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:label_query_new), {:controller => 'queries', :action => 'new', :project_id => @project}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%= l(:label_query_plural) %></h2>
+
+<% if @queries.empty? %>
+ <p><i><%=l(:label_no_data)%></i></p>
+<% else %>
+ <table class="list">
+ <% @queries.each do |query| %>
+ <tr class="<%= cycle('odd', 'even') %>">
+ <td>
+ <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %>
+ </td>
+ <td align="right">
+ <small>
+ <% if query.editable_by?(User.current) %>
+ <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => query}, :class => 'icon icon-edit' %>
+ <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
+ </small>
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+<% end %>
--- /dev/null
+<h2><%= l(:label_query_new) %></h2>
+
+<% form_tag({:action => 'new', :project_id => @query.project}, :onsubmit => 'selectAllOptions("selected_columns");') do %>
+ <%= render :partial => 'form', :locals => {:query => @query} %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% if @statuses.empty? or rows.empty? %>
+ <p><i><%=l(:label_no_data)%></i></p>
+<% else %>
+<% col_width = 70 / (@statuses.length+3) %>
+<table class="list">
+<thead><tr>
+<th style="width:25%"></th>
+<% for status in @statuses %>
+<th style="width:<%= col_width %>%"><%= status.name %></th>
+<% end %>
+<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_open_issues_plural)%></strong></th>
+<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_closed_issues_plural)%></strong></th>
+<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_total)%></strong></th>
+</tr></thead>
+<tbody>
+<% for row in rows %>
+<tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id %></td>
+ <% for status in @statuses %>
+ <td align="center"><%= aggregate_link data, { field_name => row.id, "status_id" => status.id },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "status_id" => status.id,
+ "#{field_name}" => row.id %></td>
+ <% end %>
+ <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 0 },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "o" %></td>
+ <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 1 },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "c" %></td>
+ <td align="center"><%= aggregate_link data, { field_name => row.id },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "*" %></td>
+</tr>
+<% end %>
+</tbody>
+</table>
+<% end
+ reset_cycle %>
\ No newline at end of file
--- /dev/null
+<% if @statuses.empty? or rows.empty? %>
+ <p><i><%=l(:label_no_data)%></i></p>
+<% else %>
+<table class="list">
+<thead><tr>
+<th style="width:25%"></th>
+<th align="center" style="width:25%"><%=l(:label_open_issues_plural)%></th>
+<th align="center" style="width:25%"><%=l(:label_closed_issues_plural)%></th>
+<th align="center" style="width:25%"><%=l(:label_total)%></th>
+</tr></thead>
+<tbody>
+<% for row in rows %>
+<tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id %></td>
+ <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 0 },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "o" %></td>
+ <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 1 },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "c" %></td>
+ <td align="center"><%= aggregate_link data, { field_name => row.id },
+ :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)),
+ :set_filter => 1,
+ "#{field_name}" => row.id,
+ "status_id" => "*" %></td>
+</tr>
+<% end %>
+</tbody>
+</table>
+<% end
+ reset_cycle %>
\ No newline at end of file
--- /dev/null
+<h2><%=l(:label_report_plural)%></h2>
+
+<div class="splitcontentleft">
+<h3><%=l(:field_tracker)%> <%= link_to image_tag('zoom_in.png'), :detail => 'tracker' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %>
+<br />
+<h3><%=l(:field_priority)%> <%= link_to image_tag('zoom_in.png'), :detail => 'priority' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %>
+<br />
+<h3><%=l(:field_assigned_to)%> <%= link_to image_tag('zoom_in.png'), :detail => 'assigned_to' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_assigned_to, :field_name => "assigned_to_id", :rows => @assignees } %>
+<br />
+<h3><%=l(:field_author)%> <%= link_to image_tag('zoom_in.png'), :detail => 'author' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %>
+<br />
+</div>
+
+<div class="splitcontentright">
+<h3><%=l(:field_version)%> <%= link_to image_tag('zoom_in.png'), :detail => 'version' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %>
+<br />
+<% if @project.children.any? %>
+<h3><%=l(:field_subproject)%> <%= link_to image_tag('zoom_in.png'), :detail => 'subproject' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_subproject, :field_name => "project_id", :rows => @subprojects } %>
+<br />
+<% end %>
+<h3><%=l(:field_category)%> <%= link_to image_tag('zoom_in.png'), :detail => 'category' %></h3>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %>
+<br />
+</div>
+
--- /dev/null
+<h2><%=l(:label_report_plural)%></h2>
+
+<h3><%=@report_title%></h3>
+<%= render :partial => 'details', :locals => { :data => @data, :field_name => @field, :rows => @rows } %>
+<br />
+<%= link_to l(:button_back), :action => 'issue_report' %>
+
--- /dev/null
+<%= link_to 'root', :action => 'show', :id => @project, :path => '', :rev => @rev %>
+<%
+dirs = path.split('/')
+if 'file' == kind
+ filename = dirs.pop
+end
+link_path = ''
+dirs.each do |dir|
+ next if dir.blank?
+ link_path << '/' unless link_path.empty?
+ link_path << "#{dir}"
+ %>
+ / <%= link_to h(dir), :action => 'show', :id => @project, :path => to_path_param(link_path), :rev => @rev %>
+<% end %>
+<% if filename %>
+ / <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
+<% end %>
+
+<%= "@ #{revision}" if revision %>
+
+<% html_title(with_leading_slash(path)) -%>
--- /dev/null
+<table class="list entries" id="browser">
+<thead>
+<tr id="root">
+<th><%= l(:field_name) %></th>
+<th><%= l(:field_filesize) %></th>
+<th><%= l(:label_revision) %></th>
+<th><%= l(:label_age) %></th>
+<th><%= l(:field_author) %></th>
+<th><%= l(:field_comments) %></th>
+</tr>
+</thead>
+<tbody>
+<%= render :partial => 'dir_list_content' %>
+</tbody>
+</table>
--- /dev/null
+<% @entries.each do |entry| %>
+<% tr_id = Digest::MD5.hexdigest(entry.path)
+ depth = params[:depth].to_i %>
+<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>">
+<td style="padding-left: <%=18 * depth%>px;" class="filename">
+<% if entry.is_dir? %>
+<span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
+ :method => :get,
+ :update => { :success => tr_id },
+ :position => :after,
+ :success => "scmEntryLoaded('#{tr_id}')",
+ :condition => "scmEntryClick('#{tr_id}')"%>"> </span>
+<% end %>
+<%= link_to h(entry.name),
+ {:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
+ :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(entry.name)}")%>
+</td>
+<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
+<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
+<td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
+<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
+<td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
+<td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
+</tr>
+<% end %>
--- /dev/null
+<% if @entry && @entry.kind == 'file' %>
+
+<p>
+<%= link_to_if action_name != 'changes', l(:label_history), {:action => 'changes', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
+<% if @repository.supports_cat? %>
+ <%= link_to_if action_name != 'entry', l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
+<% end %>
+<% if @repository.supports_annotate? %>
+ <%= link_to_if action_name != 'annotate', l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
+<% end %>
+<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
+<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
+</p>
+
+<% end %>
--- /dev/null
+<% content_for :header_tags do %>
+ <%= javascript_include_tag 'repository_navigation' %>
+<% end %>
+
+<%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
+
+<% form_tag({:action => controller.action_name, :id => @project, :path => to_path_param(@path), :rev => ''}, {:method => :get, :id => 'revision_selector'}) do -%>
+ <!-- Branches Dropdown -->
+ <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
+ | <%= l(:label_branch) %>:
+ <%= select_tag :branch, options_for_select([''] + @repository.branches,@rev), :id => 'branch' %>
+ <% end -%>
+
+ <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
+ | <%= l(:label_tag) %>:
+ <%= select_tag :tag, options_for_select([''] + @repository.tags,@rev), :id => 'tag' %>
+ <% end -%>
+
+ | <%= l(:label_revision) %>:
+ <%= text_field_tag 'rev', @rev, :size => 8 %>
+<% end -%>
--- /dev/null
+<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %>
+<table class="list changesets">
+<thead><tr>
+<th>#</th>
+<th></th>
+<th></th>
+<th><%= l(:label_date) %></th>
+<th><%= l(:field_author) %></th>
+<th><%= l(:field_comments) %></th>
+</tr></thead>
+<tbody>
+<% show_diff = revisions.size > 1 %>
+<% line_num = 1 %>
+<% revisions.each do |changeset| %>
+<tr class="changeset <%= cycle 'odd', 'even' %>">
+<td class="id"><%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></td>
+<td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
+<td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
+<td class="committed_on"><%= format_time(changeset.committed_on) %></td>
+<td class="author"><%=h changeset.author %></td>
+<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
+</tr>
+<% line_num += 1 %>
+<% end %>
+</tbody>
+</table>
+<%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
+<% end %>
--- /dev/null
+<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
+
+<div class="contextual">
+ <%= render :partial => 'navigation' %>
+</div>
+
+<h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
+
+<p><%= render :partial => 'link_to_functions' %></p>
+
+<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
+
+<div class="autoscroll">
+<table class="filecontent annotate CodeRay">
+ <tbody>
+ <% line_num = 1 %>
+ <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
+ <% revision = @annotate.revisions[line_num-1] %>
+ <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
+ <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
+ <td class="revision">
+ <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
+ <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
+ <td class="line-code"><pre><%= line %></pre></td>
+ </tr>
+ <% line_num += 1 %>
+ <% end %>
+ </tbody>
+</table>
+</div>
+
+<% html_title(l(:button_annotate)) -%>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag 'scm' %>
+<% end %>
--- /dev/null
+<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
+
+<div class="contextual">
+ <%= render :partial => 'navigation' %>
+</div>
+
+<h2>
+ <%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %>
+</h2>
+
+<p><%= render :partial => 'link_to_functions' %></p>
+
+<%= render_properties(@properties) %>
+
+<%= render(:partial => 'revisions',
+ :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %>
+
+<% html_title(l(:label_change_plural)) -%>
--- /dev/null
+<h2><%= l(:label_repository) %></h2>
+
+<%= simple_format(l(:text_repository_usernames_mapping)) %>
+
+<% if @committers.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% else %>
+
+<% form_tag({}) do %>
+<table class="list">
+<thead>
+ <tr>
+ <th><%= l(:field_login) %></th>
+ <th><%= l(:label_user) %></th>
+ </tr>
+</thead>
+<tbody>
+<% i = 0 -%>
+<% @committers.each do |committer, user_id| -%>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%=h committer %></td>
+ <td>
+ <%= hidden_field_tag "committers[#{i}][]", committer %>
+ <%= select_tag "committers[#{i}][]", content_tag('option', "-- #{l :actionview_instancetag_blank_option} --", :value => '') + options_from_collection_for_select(@users, 'id', 'name', user_id.to_i) %>
+ </td>
+ </tr>
+ <% i += 1 -%>
+<% end -%>
+</tbody>
+</table>
+<p><%= submit_tag(l(:button_update)) %></p>
+<% end %>
+
+<% end %>
\ No newline at end of file
--- /dev/null
+<h2><%= l(:label_revision) %> <%= format_revision(@rev_to) + ':' if @rev_to %><%= format_revision(@rev) %> <%=h @path %></h2>
+
+<!-- Choose view type -->
+<% form_tag({}, :method => 'get') do %>
+ <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
+ <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
+ <p><label><%= l(:label_view_diff) %></label>
+ <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
+<% end %>
+
+<% cache(@cache_key) do -%>
+<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
+<% end -%>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
+<% end %>
+
+<% html_title(with_leading_slash(@path), 'Diff') -%>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag "scm" %>
+<% end %>
--- /dev/null
+<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
+
+<div class="contextual">
+ <%= render :partial => 'navigation' %>
+</div>
+
+<h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
+
+<p><%= render :partial => 'link_to_functions' %></p>
+
+<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag "scm" %>
+<% end %>
--- /dev/null
+<div class="contextual">
+ «
+ <% unless @changeset.previous.nil? -%>
+ <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %>
+ <% else -%>
+ <%= l(:label_previous) %>
+ <% end -%>
+|
+ <% unless @changeset.next.nil? -%>
+ <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %>
+ <% else -%>
+ <%= l(:label_next) %>
+ <% end -%>
+ »
+
+ <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
+ <%= text_field_tag 'rev', @rev[0,8], :size => 8 %>
+ <%= submit_tag 'OK', :name => nil %>
+ <% end %>
+</div>
+
+<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
+
+<p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
+<span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
+
+<%= textilizable @changeset.comments %>
+
+<% if @changeset.issues.visible.any? %>
+<h3><%= l(:label_related_issues) %></h3>
+<ul>
+<% @changeset.issues.visible.each do |issue| %>
+ <li><%= link_to_issue issue %></li>
+<% end %>
+</ul>
+<% end %>
+
+<% if User.current.allowed_to?(:browse_repository, @project) %>
+<h3><%= l(:label_attachment_plural) %></h3>
+<ul id="changes-legend">
+<li class="change change-A"><%= l(:label_added) %></li>
+<li class="change change-M"><%= l(:label_modified) %></li>
+<li class="change change-C"><%= l(:label_copied) %></li>
+<li class="change change-R"><%= l(:label_renamed) %></li>
+<li class="change change-D"><%= l(:label_deleted) %></li>
+</ul>
+
+<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
+
+<div class="changeset-changes">
+<%= render_changeset_changes %>
+</div>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag "scm" %>
+<% end %>
+
+<% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
--- /dev/null
+<div class="contextual">
+<% form_tag({:action => 'revision', :id => @project}) do %>
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
+<%= submit_tag 'OK' %>
+<% end %>
+</div>
+
+<h2><%= l(:label_revision_plural) %></h2>
+
+<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>
+
+<p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag "scm" %>
+<%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
+<% end %>
+
+<% html_title(l(:label_revision_plural)) -%>
--- /dev/null
+<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
+
+<div class="contextual">
+ <%= render :partial => 'navigation' %>
+</div>
+
+<h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
+
+<% if !@entries.nil? && authorize_for('repositories', 'browse') %>
+<%= render :partial => 'dir_list' %>
+<% end %>
+
+<%= render_properties(@properties) %>
+
+<% if @changesets && !@changesets.empty? && authorize_for('repositories', 'revisions') %>
+<h3><%= l(:label_latest_revision_plural) %></h3>
+<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => nil }%>
+
+<% if @path.blank? %>
+ <p><%= link_to l(:label_view_all_revisions), :action => 'revisions', :id => @project %></p>
+<% else %>
+ <p><%= link_to l(:label_view_revisions), :action => 'changes', :path => to_path_param(@path), :id => @project %></p>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :action => 'revisions', :id => @project, :page => nil, :key => User.current.rss_key})) %>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:action => 'revisions', :id => @project, :key => User.current.rss_key} %>
+<% end %>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag "scm" %>
+<% end %>
+
+<% html_title(l(:label_repository)) -%>
--- /dev/null
+<h2><%= l(:label_statistics) %></h2>
+
+<p>
+<%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
+</p>
+<p>
+<%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
+</p>
+
+<p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
+
+<% html_title(l(:label_repository), l(:label_statistics)) -%>
--- /dev/null
+<%= error_messages_for 'role' %>
+
+<% unless @role.builtin? %>
+<div class="box">
+<p><%= f.text_field :name, :required => true %></p>
+<p><%= f.check_box :assignable %></p>
+<% if @role.new_record? && @roles.any? %>
+<p><label><%= l(:label_copy_workflow_from) %></label>
+<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name)) %></p>
+<% end %>
+</div>
+<% end %>
+
+<h3><%= l(:label_permissions) %></h3>
+<div class="box" id="permissions">
+<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
+<% perms_by_module.keys.sort.each do |mod| %>
+ <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
+ <% perms_by_module[mod].each do |permission| %>
+ <label class="floating">
+ <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name) %>
+ <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
+ </label>
+ <% end %>
+ </fieldset>
+<% end %>
+<br /><%= check_all_links 'permissions' %>
+<%= hidden_field_tag 'role[permissions][]', '' %>
+</div>
--- /dev/null
+<h2><%= link_to l(:label_role_plural), :controller => 'roles', :action => 'index' %> » <%=h @role.name %></h2>
+
+<% labelled_tabular_form_for :role, @role, :url => { :action => 'edit' }, :html => {:id => 'role_form'} do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_role_new), {:action => 'new'}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_role_plural)%></h2>
+
+<table class="list">
+ <thead><tr>
+ <th><%=l(:label_role)%></th>
+ <th><%=l(:button_sort)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% for role in @roles %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, :action => 'edit', :id => role)) %></td>
+ <td align="center" style="width:15%;">
+ <% unless role.builtin? %>
+ <%= reorder_links('role', {:action => 'edit', :id => role}) %>
+ <% end %>
+ </td>
+ <td class="buttons">
+ <%= link_to(l(:button_delete), { :action => 'destroy', :id => role },
+ :method => :post,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del') unless role.builtin? %>
+ </td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<p class="pagination"><%= pagination_links_full @role_pages %></p>
+
+<p><%= link_to l(:label_permissions_report), :action => 'report' %></p>
+
+<% html_title(l(:label_role_plural)) -%>
--- /dev/null
+<h2><%= link_to l(:label_role_plural), :controller => 'roles', :action => 'index' %> » <%=l(:label_role_new)%></h2>
+
+<% labelled_tabular_form_for :role, @role, :url => { :action => 'new' }, :html => {:id => 'role_form'} do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
\ No newline at end of file
--- /dev/null
+<h2><%= link_to l(:label_role_plural), :controller => 'roles', :action => 'index' %> » <%=l(:label_permissions_report)%></h2>
+
+<% form_tag({:action => 'report'}, :id => 'permissions_form') do %>
+<%= hidden_field_tag 'permissions[0]', '', :id => nil %>
+<table class="list">
+<thead>
+ <tr>
+ <th><%=l(:label_permissions)%></th>
+ <% @roles.each do |role| %>
+ <th>
+ <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
+ <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
+ </th>
+ <% end %>
+ </tr>
+</thead>
+<tbody>
+<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
+<% perms_by_module.keys.sort.each do |mod| %>
+ <% unless mod.blank? %>
+ <tr class="group open">
+ <td colspan="<%= @roles.size + 1 %>">
+ <span class="expander" onclick="toggleRowGroup(this); return false;"> </span>
+ <%= l_or_humanize(mod, :prefix => 'project_module_') %>
+ </td>
+ </tr>
+ <% end %>
+ <% perms_by_module[mod].each do |permission| %>
+ <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
+ <td>
+ <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
+ <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
+ </td>
+ <% @roles.each do |role| %>
+ <td align="center">
+ <% if role.setable_permissions.include? permission %>
+ <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
+ <% end %>
+ </td>
+ <% end %>
+ </tr>
+ <% end %>
+<% end %>
+</tbody>
+</table>
+<p><%= check_all_links 'permissions_form' %></p>
+<p><%= submit_tag l(:button_save) %></p>
+<% end %>
--- /dev/null
+<h2><%= l(:label_search) %></h2>
+
+<div class="box">
+<% form_tag({}, :method => :get) do %>
+<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
+<%= javascript_tag "Field.focus('search-input')" %>
+<%= project_select_tag %>
+<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
+<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
+</p>
+<p>
+<% @object_types.each do |t| %>
+<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
+<% end %>
+</p>
+
+<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
+<% end %>
+</div>
+
+<% if @results %>
+ <div id="search-results-counts">
+ <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
+ </div>
+
+ <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
+ <dl id="search-results">
+ <% @results.each do |e| %>
+ <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %></dt>
+ <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
+ <span class="author"><%= format_time(e.event_datetime) %></span></dd>
+ <% end %>
+ </dl>
+<% end %>
+
+<p><center>
+<% if @pagination_previous_date %>
+<%= link_to_remote ('« ' + l(:label_previous)),
+ {:update => :content,
+ :url => params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))
+ }, :href => url_for(params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>
+<% end %>
+<% if @pagination_next_date %>
+<%= link_to_remote (l(:label_next) + ' »'),
+ {:update => :content,
+ :url => params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))
+ }, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
+<% end %>
+</center></p>
+
+<% html_title(l(:label_search)) -%>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'authentication'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_login_required) %></label>
+<%= hidden_field_tag 'settings[login_required]', 0 %>
+<%= check_box_tag 'settings[login_required]', 1, Setting.login_required? %>
+</p>
+
+<p><label><%= l(:setting_autologin) %></label>
+<%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]}, Setting.autologin) %></p>
+
+<p><label><%= l(:setting_self_registration) %></label>
+<%= select_tag 'settings[self_registration]',
+ options_for_select( [[l(:label_disabled), "0"],
+ [l(:label_registration_activation_by_email), "1"],
+ [l(:label_registration_manual_activation), "2"],
+ [l(:label_registration_automatic_activation), "3"]
+ ], Setting.self_registration ) %></p>
+
+<p><label><%= l(:setting_password_min_length) %></label>
+<%= text_field_tag 'settings[password_min_length]', Setting.password_min_length, :size => 6 %></p>
+
+<p><label><%= l(:label_password_lost) %></label>
+<%= hidden_field_tag 'settings[lost_password]', 0 %>
+<%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %>
+</p>
+
+<p><label><%= l(:setting_openid) %></label>
+<%= hidden_field_tag 'settings[openid]', 0 %>
+<%= check_box_tag 'settings[openid]', 1, Setting.openid?, :disabled => !Object.const_defined?(:OpenID) %>
+</p>
+</div>
+
+<div style="float:right;">
+ <%= link_to l(:label_ldap_authentication), :controller => 'auth_sources', :action => 'list' %>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'display'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:label_theme) %></label>
+<%= select_tag 'settings[ui_theme]', options_for_select( ([[l(:label_default), '']] + Redmine::Themes.themes.collect {|t| [t.name, t.id]}), Setting.ui_theme) %></p>
+
+<p><label><%= l(:setting_default_language) %></label>
+<%= select_tag 'settings[default_language]', options_for_select( lang_options_for_select(false), Setting.default_language) %></p>
+
+<p><label><%= l(:setting_date_format) %></label>
+<%= select_tag 'settings[date_format]', options_for_select( [[l(:label_language_based), '']] + Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, Setting.date_format) %></p>
+
+<p><label><%= l(:setting_time_format) %></label>
+<%= select_tag 'settings[time_format]', options_for_select( [[l(:label_language_based), '']] + Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, Setting.time_format) %></p>
+
+<p><label><%= l(:setting_user_format) %></label>
+<%= select_tag 'settings[user_format]', options_for_select( @options[:user_format], Setting.user_format.to_s ) %></p>
+
+<p><label><%= l(:setting_gravatar_enabled) %></label>
+<%= hidden_field_tag 'settings[gravatar_enabled]', 0 %>
+<%= check_box_tag 'settings[gravatar_enabled]', 1, Setting.gravatar_enabled? %>
+</p>
+
+<p><label><%= l(:setting_gravatar_default) %></label>
+<%= select_tag 'settings[gravatar_default]', options_for_select([[l(:label_none), ''], ["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid']], Setting.gravatar_default) %></p>
+</p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_app_title) %></label>
+<%= text_field_tag 'settings[app_title]', Setting.app_title, :size => 30 %></p>
+
+<p><label><%= l(:setting_welcome_text) %></label>
+<%= text_area_tag 'settings[welcome_text]', Setting.welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p>
+<%= wikitoolbar_for 'settings[welcome_text]' %>
+
+<p><label><%= l(:setting_attachment_max_size) %></label>
+<%= text_field_tag 'settings[attachment_max_size]', Setting.attachment_max_size, :size => 6 %> KB</p>
+
+<p><label><%= l(:setting_per_page_options) %></label>
+<%= text_field_tag 'settings[per_page_options]', Setting.per_page_options_array.join(', '), :size => 20 %><br /><em><%= l(:text_comma_separated) %></em></p>
+
+<p><label><%= l(:setting_activity_days_default) %></label>
+<%= text_field_tag 'settings[activity_days_default]', Setting.activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p>
+
+<p><label><%= l(:setting_host_name) %></label>
+<%= text_field_tag 'settings[host_name]', Setting.host_name, :size => 60 %><br />
+<em><%= l(:label_example) %>: <%= @guessed_host_and_path %></em></p>
+
+<p><label><%= l(:setting_protocol) %></label>
+<%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p>
+
+<p><label><%= l(:setting_text_formatting) %></label>
+<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], *Redmine::WikiFormatting.format_names.collect{|name| [name, name]} ], Setting.text_formatting.to_sym) %></p>
+
+<p><label><%= l(:setting_wiki_compression) %></label>
+<%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p>
+
+<p><label><%= l(:setting_feeds_limit) %></label>
+<%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %></p>
+
+<p><label><%= l(:setting_file_max_size_displayed) %></label>
+<%= text_field_tag 'settings[file_max_size_displayed]', Setting.file_max_size_displayed, :size => 6 %> KB</p>
+
+<p><label><%= l(:setting_diff_max_lines_displayed) %></label>
+<%= text_field_tag 'settings[diff_max_lines_displayed]', Setting.diff_max_lines_displayed, :size => 6 %></p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'issues'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_cross_project_issue_relations) %></label>
+<%= hidden_field_tag 'settings[cross_project_issue_relations]', 0 %>
+<%= check_box_tag 'settings[cross_project_issue_relations]', 1, Setting.cross_project_issue_relations? %>
+</p>
+
+<p><label><%= l(:setting_display_subprojects_issues) %></label>
+<%= hidden_field_tag 'settings[display_subprojects_issues]', 0 %>
+<%= check_box_tag 'settings[display_subprojects_issues]', 1, Setting.display_subprojects_issues? %>
+</p>
+
+<p><label><%= l(:setting_issues_export_limit) %></label>
+<%= text_field_tag 'settings[issues_export_limit]', Setting.issues_export_limit, :size => 6 %></p>
+</div>
+
+<fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
+<%= hidden_field_tag 'settings[issue_list_default_columns][]', '' %>
+<% Query.new.available_columns.each do |column| %>
+ <label><%= check_box_tag 'settings[issue_list_default_columns][]', column.name, Setting.issue_list_default_columns.include?(column.name.to_s) %>
+ <%= column.caption %></label><br />
+<% end %>
+</fieldset>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_mail_handler_api_enabled) %></label>
+<%= hidden_field_tag 'settings[mail_handler_api_enabled]', 0 %>
+<%= check_box_tag 'settings[mail_handler_api_enabled]', 1, Setting.mail_handler_api_enabled?,
+ :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }" %>
+</p>
+
+<p><label><%= l(:setting_mail_handler_api_key) %></label>
+<%= text_field_tag 'settings[mail_handler_api_key]', Setting.mail_handler_api_key,
+ :size => 30,
+ :id => 'settings_mail_handler_api_key',
+ :disabled => !Setting.mail_handler_api_enabled? %>
+<%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %></p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% if @deliveries %>
+<% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_mail_from) %></label>
+<%= text_field_tag 'settings[mail_from]', Setting.mail_from, :size => 60 %></p>
+
+<p><label><%= l(:setting_bcc_recipients) %></label>
+<%= hidden_field_tag 'settings[bcc_recipients]', 0 %>
+<%= check_box_tag 'settings[bcc_recipients]', 1, Setting.bcc_recipients? %>
+</p>
+
+<p><label><%= l(:setting_plain_text_mail) %></label>
+<%= hidden_field_tag 'settings[plain_text_mail]', 0 %>
+<%= check_box_tag 'settings[plain_text_mail]', 1, Setting.plain_text_mail? %>
+</p>
+</div>
+
+<fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
+<% @notifiables.each do |notifiable| %>
+ <label><%= check_box_tag 'settings[notified_events][]', notifiable, Setting.notified_events.include?(notifiable) %>
+ <%= l_or_humanize(notifiable, :prefix => 'label_') %></label><br />
+<% end %>
+<%= hidden_field_tag 'settings[notified_events][]', '' %>
+<p><%= check_all_links('notified_events') %></p>
+</fieldset>
+
+<fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
+<%= text_area_tag 'settings[emails_footer]', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %>
+</fieldset>
+
+<div style="float:right;">
+<%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
+<% else %>
+<div class="nodata">
+<%= simple_format(l(:text_email_delivery_not_configured)) %>
+</div>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'projects'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_default_projects_public) %></label>
+<%= hidden_field_tag 'settings[default_projects_public]', 0 %>
+<%= check_box_tag 'settings[default_projects_public]', 1, Setting.default_projects_public? %>
+</p>
+
+<p><label><%= l(:setting_default_projects_modules) %></label>
+<%= hidden_field_tag 'settings[default_projects_modules][]', '' %>
+<% Redmine::AccessControl.available_project_modules.each do |m| %>
+ <label class="block">
+ <%= check_box_tag 'settings[default_projects_modules][]', m, Setting.default_projects_modules.include?(m.to_s) %>
+ <%= l_or_humanize(m, :prefix => "project_module_") %>
+ </label>
+<% end %>
+</p>
+
+<p><label><%= l(:setting_sequential_project_identifiers) %></label>
+<%= hidden_field_tag 'settings[sequential_project_identifiers]', 0 %>
+<%= check_box_tag 'settings[sequential_project_identifiers]', 1, Setting.sequential_project_identifiers? %>
+</p>
+
+<p><label><%= l(:setting_new_project_user_role_id) %></label>
+<%= select_tag('settings[new_project_user_role_id]', options_for_select([["--- #{l(:actionview_instancetag_blank_option)} ---", '']] + Role.find_all_givable.collect {|r| [r.name, r.id]}, Setting.new_project_user_role_id.to_i)) %></p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% form_tag({:action => 'edit', :tab => 'repositories'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_autofetch_changesets) %></label>
+<%= hidden_field_tag 'settings[autofetch_changesets]', 0 %>
+<%= check_box_tag 'settings[autofetch_changesets]', 1, Setting.autofetch_changesets? %>
+</p>
+
+<p><label><%= l(:setting_sys_api_enabled) %></label>
+<%= hidden_field_tag 'settings[sys_api_enabled]', 0 %>
+<%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %>
+</p>
+
+<p><label><%= l(:setting_enabled_scm) %></label>
+<% REDMINE_SUPPORTED_SCM.each do |scm| -%>
+<%= check_box_tag 'settings[enabled_scm][]', scm, Setting.enabled_scm.include?(scm) %> <%= scm %>
+<% end -%>
+<%= hidden_field_tag 'settings[enabled_scm][]', '' %>
+</p>
+
+<p><label><%= l(:setting_repositories_encodings) %></label>
+<%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %><br /><em><%= l(:text_comma_separated) %></em></p>
+
+<p><label><%= l(:setting_commit_logs_encoding) %></label>
+<%= select_tag 'settings[commit_logs_encoding]', options_for_select(Setting::ENCODINGS, Setting.commit_logs_encoding) %></p>
+
+<p><label><%= l(:setting_repository_log_display_limit) %></label>
+<%= text_field_tag 'settings[repository_log_display_limit]', Setting.repository_log_display_limit, :size => 6 %></p>
+</div>
+
+<fieldset class="box tabular settings"><legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
+<p><label><%= l(:setting_commit_ref_keywords) %></label>
+<%= text_field_tag 'settings[commit_ref_keywords]', Setting.commit_ref_keywords, :size => 30 %><br /><em><%= l(:text_comma_separated) %></em></p>
+
+<p><label><%= l(:setting_commit_fix_keywords) %></label>
+<%= text_field_tag 'settings[commit_fix_keywords]', Setting.commit_fix_keywords, :size => 30 %>
+ <%= l(:label_applied_status) %>: <%= select_tag 'settings[commit_fix_status_id]', options_for_select( [["", 0]] + IssueStatus.find(:all).collect{|status| [status.name, status.id.to_s]}, Setting.commit_fix_status_id) %>
+ <%= l(:field_done_ratio) %>: <%= select_tag 'settings[commit_fix_done_ratio]', options_for_select( [[l(:label_no_change_option), '']] + ((0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] }), Setting.commit_fix_done_ratio) %>
+<br /><em><%= l(:text_comma_separated) %></em></p>
+</fieldset>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<h2><%= l(:label_settings) %></h2>
+
+<%= render_tabs administration_settings_tabs %>
+
+<% html_title(l(:label_settings), l(:label_administration)) -%>
--- /dev/null
+<h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
+
+<div id="settings">
+<% form_tag({:action => 'plugin'}) do %>
+<div class="box tabular">
+<%= render :partial => @partial, :locals => {:settings => @settings}%>
+</div>
+<%= submit_tag l(:button_apply) %>
+<% end %>
+</div>
--- /dev/null
+<fieldset id="date-range" class="collapsible">
+<legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
+<div>
+<p>
+<%= radio_button_tag 'period_type', '1', !@free_period %>
+<%= select_tag 'period', options_for_period_select(params[:period]),
+ :onchange => 'this.form.onsubmit();',
+ :onfocus => '$("period_type_1").checked = true;' %>
+</p>
+<p>
+<%= radio_button_tag 'period_type', '2', @free_period %>
+<span onclick="$('period_type_2').checked = true;">
+<%= l(:label_date_from_to, :start => (text_field_tag('from', @from, :size => 10) + calendar_for('from')),
+ :end => (text_field_tag('to', @to, :size => 10) + calendar_for('to'))) %>
+</span>
+</p>
+</div>
+</fieldset>
+<p class="buttons">
+ <%= link_to_remote l(:button_apply),
+ { :url => { },
+ :update => "content",
+ :with => "Form.serialize('query_form')",
+ :method => :get
+ }, :class => 'icon icon-checked' %>
+</p>
+
+<div class="tabs">
+<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
+<ul>
+ <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue }),
+ :class => (@controller.action_name == 'details' ? 'selected' : nil)) %></li>
+ <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
+ :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li>
+</ul>
+</div>
--- /dev/null
+<table class="list time-entries">
+<thead>
+<tr>
+<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
+<%= sort_header_tag('user', :caption => l(:label_member)) %>
+<%= sort_header_tag('activity', :caption => l(:label_activity)) %>
+<%= sort_header_tag('project', :caption => l(:label_project)) %>
+<%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
+<th><%= l(:field_comments) %></th>
+<%= sort_header_tag('hours', :caption => l(:field_hours)) %>
+<th></th>
+</tr>
+</thead>
+<tbody>
+<% entries.each do |entry| -%>
+<tr class="time-entry <%= cycle("odd", "even") %>">
+<td class="spent_on"><%= format_date(entry.spent_on) %></td>
+<td class="user"><%=h entry.user %></td>
+<td class="activity"><%=h entry.activity %></td>
+<td class="project"><%=h entry.project %></td>
+<td class="subject">
+<% if entry.issue -%>
+<%= entry.issue.visible? ? link_to_issue(entry.issue, :truncate => 50) : "##{entry.issue.id}" -%>
+<% end -%>
+</td>
+<td class="comments"><%=h entry.comments %></td>
+<td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
+<td align="center">
+<% if entry.editable_by?(User.current) -%>
+ <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
+ :title => l(:button_edit) %>
+ <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
+ :confirm => l(:text_are_you_sure),
+ :method => :post,
+ :title => l(:button_delete) %>
+<% end -%>
+</td>
+</tr>
+<% end -%>
+</tbody>
+</table>
--- /dev/null
+<% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
+<% hours_for_value = select_hours(hours, criterias[level], value) -%>
+<% next if hours_for_value.empty? -%>
+<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
+<%= '<td></td>' * level %>
+<td><%= h(format_criteria_value(criterias[level], value)) %></td>
+<%= '<td></td>' * (criterias.length - level - 1) -%>
+ <% total = 0 -%>
+ <% @periods.each do |period| -%>
+ <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%>
+ <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
+ <% end -%>
+ <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
+</tr>
+<% if criterias.length > level+1 -%>
+ <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
+<% end -%>
+
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
+</div>
+
+<%= render_timelog_breadcrumb %>
+
+<h2><%= l(:label_spent_time) %></h2>
+
+<% form_remote_tag( :url => {}, :html => {:method => :get, :id => 'query_form'}, :method => :get, :update => 'content' ) do %>
+<%# TOOD: remove the project_id and issue_id hidden fields, that information is
+already in the URI %>
+<%= hidden_field_tag('project_id', params[:project_id]) if @project %>
+<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
+<%= render :partial => 'date_range' %>
+<% end %>
+
+<div class="total-hours">
+<p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
+</div>
+
+<% unless @entries.empty? %>
+<%= render :partial => 'list', :locals => { :entries => @entries }%>
+<p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
+ <%= f.link_to 'CSV', :url => params %>
+<% end %>
+<% end %>
+
+<% html_title l(:label_spent_time), l(:label_details) %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
+<% end %>
--- /dev/null
+<h2><%= l(:label_spent_time) %></h2>
+
+<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :id => @time_entry, :project_id => @time_entry.project} do |f| %>
+<%= error_messages_for 'time_entry' %>
+<%= back_url_hidden_field_tag %>
+
+<div class="box">
+<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
+<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
+<p><%= f.text_field :hours, :size => 6, :required => true %></p>
+<p><%= f.text_field :comments, :size => 100 %></p>
+<p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
+<% @time_entry.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :time_entry, value %></p>
+<% end %>
+<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
+</div>
+
+<%= submit_tag l(:button_save) %>
+
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
+</div>
+
+<%= render_timelog_breadcrumb %>
+
+<h2><%= l(:label_spent_time) %></h2>
+
+<% form_remote_tag(:url => {}, :html => {:method => :get, :id => 'query_form'}, :method => :get, :update => 'content') do %>
+ <% @criterias.each do |criteria| %>
+ <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
+ <% end %>
+ <%# TODO: get rid of the project_id field, that should already be in the URL %>
+ <%= hidden_field_tag('project_id', params[:project_id]) if @project %>
+ <%= hidden_field_tag('issue_id', params[:issue_id]) if @issue %>
+ <%= render :partial => 'date_range' %>
+
+ <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
+ [l(:label_month), 'month'],
+ [l(:label_week), 'week'],
+ [l(:label_day_plural).titleize, 'day']], @columns),
+ :onchange => "this.form.onsubmit();" %>
+
+ <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l_or_humanize(@available_criterias[k][:label]), k]}),
+ :onchange => "this.form.onsubmit();",
+ :style => 'width: 200px',
+ :id => nil,
+ :disabled => (@criterias.length >= 3)) %>
+ <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
+ :method => :get,
+ :update => 'content'
+ }, :class => 'icon icon-reload' %></p>
+<% end %>
+
+<% unless @criterias.empty? %>
+<div class="total-hours">
+<p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
+</div>
+
+<% unless @hours.empty? %>
+<table class="list" id="time-report">
+<thead>
+<tr>
+<% @criterias.each do |criteria| %>
+ <th><%= l_or_humanize(@available_criterias[criteria][:label]) %></th>
+<% end %>
+<% columns_width = (40 / (@periods.length+1)).to_i %>
+<% @periods.each do |period| %>
+ <th class="period" width="<%= columns_width %>%"><%= period %></th>
+<% end %>
+ <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
+</tr>
+</thead>
+<tbody>
+<%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
+ <tr class="total">
+ <td><%= l(:label_total) %></td>
+ <%= '<td></td>' * (@criterias.size - 1) %>
+ <% total = 0 -%>
+ <% @periods.each do |period| -%>
+ <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
+ <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
+ <% end -%>
+ <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
+ </tr>
+</tbody>
+</table>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'CSV', :url => params %>
+<% end %>
+<% end %>
+<% end %>
+
+<% html_title l(:label_spent_time), l(:label_report) %>
+
--- /dev/null
+<%= error_messages_for 'tracker' %>
+
+<div class="splitcontentleft">
+<div class="box tabular">
+<!--[form:tracker]-->
+<p><%= f.text_field :name, :required => true %></p>
+<p><%= f.check_box :is_in_chlog %></p>
+<p><%= f.check_box :is_in_roadmap %></p>
+<% if @tracker.new_record? && @trackers.any? %>
+<p><label><%= l(:label_copy_workflow_from) %></label>
+<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@trackers, :id, :name)) %></p>
+<% end %>
+<!--[eoform:tracker]-->
+</div>
+</div>
+
+<div class="splitcontentright">
+<% if @projects.any? %>
+<fieldset class="box" id="tracker_project_ids"><legend><%= l(:label_project_plural) %></legend>
+<%= project_nested_ul(@projects) do |p|
+ content_tag('label', check_box_tag('tracker[project_ids][]', p.id, @tracker.projects.include?(p), :id => nil) + ' ' + h(p))
+end %>
+<%= hidden_field_tag('tracker[project_ids][]', '', :id => nil) %>
+<p><%= check_all_links 'tracker_project_ids' %></p>
+</fieldset>
+<% end %>
+</div>
--- /dev/null
+<h2><%= link_to l(:label_tracker_plural), :controller => 'trackers', :action => 'index' %> » <%=h @tracker %></h2>
+
+<% form_for :tracker, @tracker, :url => { :action => 'edit' }, :builder => TabularFormBuilder do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:label_tracker_new), {:action => 'new'}, :class => 'icon icon-add' %>
+</div>
+
+<h2><%=l(:label_tracker_plural)%></h2>
+
+<table class="list">
+ <thead><tr>
+ <th><%=l(:label_tracker)%></th>
+ <th></th>
+ <th><%=l(:button_sort)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% for tracker in @trackers %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= link_to tracker.name, :action => 'edit', :id => tracker %></td>
+ <td align="center"><% unless tracker.workflows.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
+ <td align="center" style="width:15%;"><%= reorder_links('tracker', {:action => 'edit', :id => tracker}) %></td>
+ <td class="buttons">
+ <%= link_to(l(:button_delete), { :action => 'destroy', :id => tracker },
+ :method => :post,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-del') %>
+ </td>
+ </tr>
+<% end %>
+ </tbody>
+</table>
+
+<p class="pagination"><%= pagination_links_full @tracker_pages %></p>
+
+<% html_title(l(:label_tracker_plural)) -%>
--- /dev/null
+<h2><%= link_to l(:label_tracker_plural), :controller => 'trackers', :action => 'index' %> » <%=l(:label_tracker_new)%></h2>
+
+<% form_for :tracker, @tracker, :url => { :action => 'new' }, :builder => TabularFormBuilder do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
--- /dev/null
+<%= error_messages_for 'user' %>
+
+<!--[form:user]-->
+<div class="box tabular">
+<p><%= f.text_field :login, :required => true, :size => 25 %></p>
+<p><%= f.text_field :firstname, :required => true %></p>
+<p><%= f.text_field :lastname, :required => true %></p>
+<p><%= f.text_field :mail, :required => true %></p>
+<p><%= f.select :language, lang_options_for_select %></p>
+<% if Setting.openid? %>
+<p><%= f.text_field :identity_url %></p>
+<% end %>
+
+<% @user.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :user, value %></p>
+<% end %>
+
+<p><%= f.check_box :admin, :disabled => (@user == User.current) %></p>
+<%= call_hook(:view_users_form, :user => @user, :form => f) %>
+</div>
+
+<div class="box tabular">
+<h3><%=l(:label_authentication)%></h3>
+<% unless @auth_sources.empty? %>
+<p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }), {}, :onchange => "if (this.value=='') {Element.show('password_fields');} else {Element.hide('password_fields');}" %></p>
+<% end %>
+<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
+<p><label for="password"><%=l(:field_password)%><span class="required"> *</span></label>
+<%= password_field_tag 'password', nil, :size => 25 %><br />
+<em><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
+<p><label for="password_confirmation"><%=l(:field_password_confirmation)%><span class="required"> *</span></label>
+<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
+</div>
+</div>
+<!--[eoform:user]-->
--- /dev/null
+<% labelled_tabular_form_for :user, @user, :url => { :controller => 'users', :action => "edit", :tab => nil }, :html => { :class => nil } do |f| %>
+ <%= render :partial => 'form', :locals => { :f => f } %>
+ <% if @user.active? -%>
+ <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label>
+ <% end -%>
+ <p><%= submit_tag l(:button_save) %></p>
+<% end %>
--- /dev/null
+<% form_for(:user, :url => { :action => 'edit' }) do %>
+<div class="box">
+<% Group.all.each do |group| %>
+<label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
+<% end %>
+<%= hidden_field_tag 'user[group_ids][]', '' %>
+</div>
+<%= submit_tag l(:button_save) %>
+<% end %>
--- /dev/null
+<% roles = Role.find_all_givable %>
+<% projects = Project.active.find(:all, :order => 'lft') %>
+
+<div class="splitcontentleft">
+<% if @user.memberships.any? %>
+<table class="list memberships">
+ <thead>
+ <th><%= l(:label_project) %></th>
+ <th><%= l(:label_role_plural) %></th>
+ <th style="width:15%"></th>
+ <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
+ </thead>
+ <tbody>
+ <% @user.memberships.each do |membership| %>
+ <% next if membership.new_record? %>
+ <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
+ <td class="project"><%=h membership.project %></td>
+ <td class="roles">
+ <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
+ <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
+ :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
+ <p><% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
+ :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
+ <% end %></p>
+ <%= hidden_field_tag 'membership[role_ids][]', '' %>
+ <p><%= submit_tag l(:button_change) %>
+ <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
+ <% end %>
+ </td>
+ <td class="buttons">
+ <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
+ <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
+ :method => :post },
+ :class => 'icon icon-del') if membership.deletable? %>
+ </td>
+ <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
+ </tr>
+ </tbody>
+<% end; reset_cycle %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<% if projects.any? %>
+<fieldset><legend><%=l(:label_project_new)%></legend>
+<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user }) do %>
+<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
+<p><%= l(:label_role_plural) %>:
+<% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
+<% end %></p>
+<p><%= submit_tag l(:button_add) %></p>
+<% end %>
+</fieldset>
+<% end %>
+</div>
--- /dev/null
+<h2><%= link_to l(:label_user_plural), :controller => 'users', :action => 'index' %> » <%=l(:label_user_new)%></h2>
+
+<% labelled_tabular_form_for :user, @user, :url => { :action => "add" }, :html => { :class => nil } do |f| %>
+ <%= render :partial => 'form', :locals => { :f => f } %>
+ <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
+ <p><%= submit_tag l(:button_create) %></p>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= change_status_link(@user) %>
+</div>
+
+<h2><%= link_to l(:label_user_plural), :controller => 'users', :action => 'index' %> » <%=h @user.login %></h2>
+
+<%= render_tabs user_settings_tabs %>
+
+<% html_title(l(:label_user), @user.login, l(:label_administration)) -%>
--- /dev/null
+<div class="contextual">\r
+<%= link_to l(:label_user_new), {:action => 'add'}, :class => 'icon icon-add' %>\r
+</div>\r
+\r
+<h2><%=l(:label_user_plural)%></h2>\r
+\r
+<% form_tag({}, :method => :get) do %>\r
+<fieldset><legend><%= l(:label_filter_plural) %></legend>\r
+<label><%= l(:field_status) %>:</label>\r
+<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>\r
+<label><%= l(:label_user) %>:</label>\r
+<%= text_field_tag 'name', params[:name], :size => 30 %>\r
+<%= submit_tag l(:button_apply), :class => "small", :name => nil %>\r
+</fieldset>\r
+<% end %>\r
+ \r
+\r
+<table class="list"> \r
+ <thead><tr>\r
+ <%= sort_header_tag('login', :caption => l(:field_login)) %>\r
+ <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>\r
+ <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>\r
+ <%= sort_header_tag('mail', :caption => l(:field_mail)) %>\r
+ <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>\r
+ <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>\r
+ <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>\r
+ <th></th>\r
+ </tr></thead>\r
+ <tbody>\r
+<% for user in @users -%>\r
+ <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">\r
+ <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), :action => 'edit', :id => user %></td>\r
+ <td class="firstname"><%= h(user.firstname) %></td>\r
+ <td class="lastname"><%= h(user.lastname) %></td>\r
+ <td class="email"><%= mail_to(h(user.mail)) %></td>\r
+ <td align="center"><%= image_tag('true.png') if user.admin? %></td>\r
+ <td class="created_on" align="center"><%= format_time(user.created_on) %></td>\r
+ <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>\r
+ <td><small><%= change_status_link(user) %></small></td>\r
+ </tr>\r
+<% end -%>\r
+ </tbody>\r
+</table>\r
+\r
+<p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>\r
+\r
+<% html_title(l(:label_user_plural)) -%>\r
--- /dev/null
+<div class="contextual">\r
+<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>\r
+</div>\r
+\r
+<h2><%= avatar @user %> <%=h @user.name %></h2>\r
+\r
+<div class="splitcontentleft">\r
+<ul>\r
+ <% unless @user.pref.hide_mail %>\r
+ <li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>\r
+ <% end %>\r
+ <% for custom_value in @custom_values %>\r
+ <% if !custom_value.value.blank? %>\r
+ <li><%=h custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>\r
+ <% end %>\r
+ <% end %>\r
+ <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>\r
+ <% unless @user.last_login_on.nil? %>\r
+ <li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>\r
+ <% end %>\r
+</ul>\r
+\r
+<% unless @memberships.empty? %>\r
+<h3><%=l(:label_project_plural)%></h3>\r
+<ul>\r
+<% for membership in @memberships %>\r
+ <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>\r
+ (<%=h membership.roles.sort.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)</li>\r
+<% end %>\r
+</ul>\r
+<% end %>\r
+<%= call_hook :view_account_left_bottom, :user => @user %>\r
+</div>\r
+\r
+<div class="splitcontentright">\r
+\r
+<% unless @events_by_day.empty? %>\r
+<h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :from => @events_by_day.keys.first %></h3>\r
+\r
+<p>\r
+<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>\r
+</p>\r
+\r
+<div id="activity">\r
+<% @events_by_day.keys.sort.reverse.each do |day| %>\r
+<h4><%= format_activity_day(day) %></h4>\r
+<dl>\r
+<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>\r
+ <dt class="<%= e.event_type %>">\r
+ <span class="time"><%= format_time(e.event_datetime, false) %></span>\r
+ <%= content_tag('span', h(e.project), :class => 'project') %>\r
+ <%= link_to format_activity_title(e.event_title), e.event_url %></dt>\r
+ <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>\r
+<% end -%>\r
+</dl>\r
+<% end -%>\r
+</div>\r
+\r
+<% other_formats_links do |f| %>\r
+ <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %>\r
+<% end %>\r
+\r
+<% content_for :header_tags do %>\r
+ <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>\r
+<% end %>\r
+<% end %>\r
+<%= call_hook :view_account_right_bottom, :user => @user %>\r
+</div>\r
+\r
+<% html_title @user.name %>\r
--- /dev/null
+<%= error_messages_for 'version' %>
+
+<div class="box">
+<p><%= f.text_field :name, :size => 60, :required => true %></p>
+<p><%= f.text_field :description, :size => 60 %></p>
+<p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
+<p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
+<p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
+
+<% @version.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :version, value %></p>
+<% end %>
+</div>
--- /dev/null
+<form id="status_by_form">
+<fieldset>
+<legend>
+<%= l(:label_issues_by,
+ select_tag('status_by',
+ status_by_options_for_select(criteria),
+ :id => 'status_by_select',
+ :onchange => remote_function(:url => { :action => :status_by, :id => version },
+ :with => "Form.serialize('status_by_form')"))) %>
+</legend>
+<% if counts.empty? %>
+ <p><em><%= l(:label_no_data) %></em></p>
+<% else %>
+ <table>
+ <% counts.each do |count| %>
+ <tr>
+ <td width="130px" align="right" >
+ <%= link_to count[:group], {:controller => 'issues',
+ :action => 'index',
+ :project_id => version.project,
+ :set_filter => 1,
+ :fixed_version_id => version,
+ "#{criteria}_id" => count[:group]} %>
+ </td>
+ <td width="240px">
+ <%= progress_bar((count[:closed].to_f / count[:total])*100,
+ :legend => "#{count[:closed]}/#{count[:total]}",
+ :width => "#{(count[:total].to_f / max * 200).floor}px;") %>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+<% end %>
+</fieldset>
+</form>
--- /dev/null
+<% if version.completed? %>
+ <p><%= format_date(version.effective_date) %></p>
+<% elsif version.effective_date %>
+ <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p>
+<% end %>
+
+<p><%=h version.description %></p>
+<ul>
+ <% version.custom_values.each do |custom_value| %>
+ <% if !custom_value.value.blank? %>
+ <li><%=h custom_value.custom_field.name %>: <%=h show_value(custom_value) %></li>
+ <% end %>
+ <% end %>
+</ul>
+
+<% if version.fixed_issues.count > 0 %>
+ <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>
+ <p class="progress-info">
+ <%= link_to_if(version.closed_issues_count > 0, l(:label_x_closed_issues_abbr, :count => version.closed_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %>
+ (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)
+  
+ <%= link_to_if(version.open_issues_count > 0, l(:label_x_open_issues_abbr, :count => version.open_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %>
+ (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)
+ </p>
+<% else %>
+ <p><em><%= l(:label_roadmap_no_issues) %></em></p>
+<% end %>
--- /dev/null
+<h2><%=l(:label_version)%></h2>
+
+<% labelled_tabular_form_for :version, @version, :url => { :action => 'edit' } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
+
--- /dev/null
+<div class="contextual">
+<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %>
+<%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
+</div>
+
+<h2><%= h(@version.name) %></h2>
+
+<div id="version-summary">
+<% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
+<fieldset><legend><%= l(:label_time_tracking) %></legend>
+<table>
+<tr>
+ <td width="130px" align="right"><%= l(:field_estimated_hours) %></td>
+ <td width="240px" class="total-hours"width="130px" align="right"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
+</tr>
+<% if User.current.allowed_to?(:view_time_entries, @project) %>
+<tr>
+ <td width="130px" align="right"><%= l(:label_spent_time) %></td>
+ <td width="240px" class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td>
+</tr>
+<% end %>
+</table>
+</fieldset>
+<% end %>
+
+<div id="status_by">
+<%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
+</div>
+</div>
+
+<div id="roadmap">
+<%= render :partial => 'versions/overview', :locals => {:version => @version} %>
+<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
+
+<% issues = @version.fixed_issues.find(:all,
+ :include => [:status, :tracker, :priority],
+ :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
+<% if issues.size > 0 %>
+<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
+<ul>
+<% issues.each do |issue| -%>
+ <li><%= link_to_issue(issue) %></li>
+<% end -%>
+</ul>
+</fieldset>
+<% end %>
+</div>
+
+<%= call_hook :view_versions_show_bottom, :version => @version %>
+
+<% html_title @version.name %>
--- /dev/null
+<div class="contextual">
+<%= link_to_remote l(:button_add),
+ :url => {:controller => 'watchers',
+ :action => 'new',
+ :object_type => watched.class.name.underscore,
+ :object_id => watched} if User.current.allowed_to?(:add_issue_watchers, @project) %>
+</div>
+
+<p><strong><%= l(:label_issue_watchers) %></strong></p>
+<%= watchers_list(watched) %>
+
+<% unless @watcher.nil? %>
+<% remote_form_for(:watcher, @watcher,
+ :url => {:controller => 'watchers',
+ :action => 'new',
+ :object_type => watched.class.name.underscore,
+ :object_id => watched},
+ :method => :post,
+ :html => {:id => 'new-watcher-form'}) do |f| %>
+<p><%= f.select :user_id, (watched.addable_watcher_users.collect {|m| [m.name, m.id]}), :prompt => true %>
+
+<%= submit_tag l(:button_add) %>
+<%= toggle_link l(:button_cancel), 'new-watcher-form'%></p>
+<% end %>
+<% end %>
--- /dev/null
+<h2><%= l(:label_home) %></h2>
+
+<div class="splitcontentleft">
+ <%= textilizable Setting.welcome_text %>
+ <% if @news.any? %>
+ <div class="box">
+ <h3><%=l(:label_news_latest)%></h3>
+ <%= render :partial => 'news/news', :collection => @news %>
+ <%= link_to l(:label_news_view_all), :controller => 'news' %>
+ </div>
+ <% end %>
+ <%= call_hook(:view_welcome_index_left, :projects => @projects) %>
+</div>
+
+<div class="splitcontentright">
+ <% if @projects.any? %>
+ <div class="box">
+ <h3 class="icon22 icon22-projects"><%=l(:label_project_latest)%></h3>
+ <ul>
+ <% for project in @projects %>
+ <li>
+ <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>)
+ <%= textilizable project.short_description, :project => project %>
+ </li>
+ <% end %>
+ </ul>
+ </div>
+ <% end %>
+ <%= call_hook(:view_welcome_index_right, :projects => @projects) %>
+</div>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom, {:controller => 'news', :action => 'index', :key => User.current.rss_key, :format => 'atom'},
+ :title => "#{Setting.app_title}: #{l(:label_news_latest)}") %>
+<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :key => User.current.rss_key, :format => 'atom'},
+ :title => "#{Setting.app_title}: #{l(:label_activity)}") %>
+<% end %>
--- /dev/null
+User-agent: *
+<% @projects.each do |p| -%>
+Disallow: /projects/<%= p.to_param %>/repository
+Disallow: /projects/<%= p.to_param %>/issues
+Disallow: /projects/<%= p.to_param %>/activity
+<% end -%>
+Disallow: /issues/gantt
+Disallow: /issues/calendar
+Disallow: /activity
--- /dev/null
+<div class="wiki">
+ <%= textilizable content, :text, :attachments => content.page.attachments %>
+</div>
--- /dev/null
+<h3><%= l(:label_wiki) %></h3>
+
+<%= link_to l(:field_start_page), {:action => 'index', :page => nil} %><br />
+<%= link_to l(:label_index_by_title), {:action => 'special', :page => 'Page_index'} %><br />
+<%= link_to l(:label_index_by_date), {:action => 'special', :page => 'Date_index'} %><br />
--- /dev/null
+<div class="contextual">
+<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %>
+<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
+</div>
+
+<h2><%= @page.pretty_title %></h2>
+
+<p>
+<%= l(:label_version) %> <%= link_to @annotate.content.version, :action => 'index', :page => @page.title, :version => @annotate.content.version %>
+<em>(<%= @annotate.content.author ? @annotate.content.author.name : "anonyme" %>, <%= format_time(@annotate.content.updated_on) %>)</em>
+</p>
+
+<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
+
+<table class="filecontent annotate CodeRay ">
+<tbody>
+<% line_num = 1 %>
+<% @annotate.lines.each do |line| -%>
+<tr class="bloc-<%= colors[line[0]] %>">
+ <th class="line-num"><%= line_num %></th>
+ <td class="revision"><%= link_to line[0], :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title, :version => line[0] %></td>
+ <td class="author"><%= h(line[1]) %></td>
+ <td class="line-code"><pre><%=h line[2] %></pre></td>
+</tr>
+<% line_num += 1 %>
+<% end -%>
+</tbody>
+</table>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag 'scm' %>
+<% end %>
--- /dev/null
+<h2><%=h @page.pretty_title %></h2>
+
+<% form_tag({}) do %>
+<div class="box">
+<p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
+<p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
+<label><%= radio_button_tag 'todo', 'destroy', false %> <%= l(:text_wiki_page_destroy_children) %></label>
+<% if @reassignable_to.any? %>
+<br />
+<label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_wiki_page_reassign_children) %></label>:
+<%= select_tag 'reassign_to_id', wiki_page_options_for_select(@reassignable_to),
+ :onclick => "$('todo_reassign').checked = true;" %>
+<% end %>
+</p>
+</div>
+
+<%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %>
+<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
+</div>
+
+<h2><%= @page.pretty_title %></h2>
+
+<p>
+<%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'index', :page => @page.title, :version => @diff.content_from.version %>
+<em>(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>)</em>
+→
+<%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'index', :page => @page.title, :version => @diff.content_to.version %>/<%= @page.content.version %>
+<em>(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>)</em>
+</p>
+
+<hr />
+
+<%= html_diff(@diff) %>
--- /dev/null
+<h2><%=h @page.pretty_title %></h2>
+
+<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %>
+<%= f.hidden_field :version %>
+<%= error_messages_for 'content' %>
+
+<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>
+<p><label><%= l(:field_comments) %></label><br /><%= f.text_field :comments, :size => 120 %></p>
+<p><%= submit_tag l(:button_save) %>
+ <%= link_to_remote l(:label_preview),
+ { :url => { :controller => 'wiki', :action => 'preview', :id => @project, :page => @page.title },
+ :method => 'post',
+ :update => 'preview',
+ :with => "Form.serialize('wiki_form')",
+ :complete => "Element.scrollTo('preview')"
+ }, :accesskey => accesskey(:preview) %></p>
+<%= wikitoolbar_for 'content_text' %>
+<% end %>
+
+<div id="preview" class="wiki"></div>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
+
+<% html_title @page.pretty_title %>
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title><%=h @page.pretty_title %></title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<style>
+body { font:80% Verdana,Tahoma,Arial,sans-serif; }
+h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; }
+ul.toc { padding: 4px; margin-left: 0; }
+ul.toc li { list-style-type:none; }
+ul.toc li.heading2 { margin-left: 1em; }
+ul.toc li.heading3 { margin-left: 2em; }
+a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
+a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
+h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
+</style>
+</head>
+<body>
+<%= textilizable @content, :text, :wiki_links => :local %>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title><%=h @wiki.project.name %></title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<style>
+body { font:80% Verdana,Tahoma,Arial,sans-serif; }
+h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; }
+ul.toc { padding: 4px; margin-left: 0; }
+ul.toc li { list-style-type:none; }
+ul.toc li.heading2 { margin-left: 1em; }
+ul.toc li.heading3 { margin-left: 2em; }
+a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
+a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
+h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
+</style>
+</head>
+<body>
+
+<strong><%= l(:label_index_by_title) %></strong>
+<ul>
+<% @pages.each do |page| %>
+ <li><a href="#<%= page.title %>"><%= page.pretty_title %></a></li>
+<% end %>
+</ul>
+
+<% @pages.each do |page| %>
+<hr />
+<a name="<%= page.title %>" />
+<%= textilizable page.content ,:text, :wiki_links => :anchor %>
+<% end %>
+
+</body>
+</html>
--- /dev/null
+<h2><%= @page.pretty_title %></h2>
+
+<h3><%= l(:label_history) %></h3>
+
+<% form_tag({:action => "diff"}, :method => :get) do %>
+<table class="list">
+<thead><tr>
+ <th>#</th>
+ <th></th>
+ <th></th>
+ <th><%= l(:field_updated_on) %></th>
+ <th><%= l(:field_author) %></th>
+ <th><%= l(:field_comments) %></th>
+ <th></th>
+</tr></thead>
+<tbody>
+<% show_diff = @versions.size > 1 %>
+<% line_num = 1 %>
+<% @versions.each do |ver| %>
+<tr class="<%= cycle("odd", "even") %>">
+ <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></td>
+ <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
+ <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true || $('version_from').value > #{ver.version}) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
+ <td align="center"><%= format_time(ver.updated_on) %></td>
+ <td><em><%= ver.author ? ver.author.name : "anonyme" %></em></td>
+ <td><%=h ver.comments %></td>
+ <td align="center"><%= link_to l(:button_annotate), :action => 'annotate', :page => @page.title, :version => ver.version %></td>
+</tr>
+<% line_num += 1 %>
+<% end %>
+</tbody>
+</table>
+<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
+<span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
+<% end %>
--- /dev/null
+<h2><%= l(:button_rename) %>: <%= @original_title %></h2>
+
+<%= error_messages_for 'page' %>
+
+<% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %>
+<div class="box">
+<p><%= f.text_field :title, :required => true, :size => 100 %></p>
+<p><%= f.check_box :redirect_existing_links %></p>
+<p><%= f.text_field :parent_title, :size => 100 %></p>
+</div>
+<%= submit_tag l(:button_rename) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<% if @editable %>
+<%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
+<%= watcher_tag(@page, User.current) %>
+<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
+<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
+<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
+<%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
+<%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
+<% end %>
+<%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
+</div>
+
+<%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
+
+<% if @content.version != @page.content.version %>
+ <p>
+ <%= link_to(('« ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
+ <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
+ <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> -
+ <%= link_to((l(:label_next) + ' »'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
+ <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
+ <br />
+ <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
+ <%=h @content.comments %>
+ </p>
+ <hr />
+<% end %>
+
+<%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
+
+<%= link_to_attachments @page %>
+
+<% if @editable && authorize_for('wiki', 'add_attachment') %>
+<div id="wiki_add_attachment">
+<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
+ :id => 'attach_files_link' %></p>
+<% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
+ <div class="box">
+ <p><%= render :partial => 'attachments/form' %></p>
+ </div>
+<%= submit_tag l(:button_add) %>
+<%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
+<% end %>
+</div>
+<% end %>
+
+<% other_formats_links do |f| %>
+ <%= f.link_to 'HTML', :url => {:page => @page.title, :version => @content.version} %>
+ <%= f.link_to 'TXT', :url => {:page => @page.title, :version => @content.version} %>
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'sidebar' %>
+<% end %>
+
+<% html_title @page.pretty_title %>
--- /dev/null
+<div class="contextual">
+<%= watcher_tag(@wiki, User.current) %>
+</div>
+
+<h2><%= l(:label_index_by_date) %></h2>
+
+<% if @pages.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<% @pages_by_date.keys.sort.reverse.each do |date| %>
+<h3><%= format_date(date) %></h3>
+<ul>
+<% @pages_by_date[date].each do |page| %>
+ <li><%= link_to page.pretty_title, :action => 'index', :page => page.title %></li>
+<% end %>
+</ul>
+<% end %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'sidebar' %>
+<% end %>
+
+<% unless @pages.empty? %>
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :key => User.current.rss_key} %>
+ <%= f.link_to 'HTML', :url => {:action => 'special', :page => 'export'} %>
+<% end %>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key) %>
+<% end %>
--- /dev/null
+<div class="contextual">
+<%= watcher_tag(@wiki, User.current) %>
+</div>
+
+<h2><%= l(:label_index_by_title) %></h2>
+
+<% if @pages.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<%= render_page_hierarchy(@pages_by_parent_id) %>
+
+<% content_for :sidebar do %>
+ <%= render :partial => 'sidebar' %>
+<% end %>
+
+<% unless @pages.empty? %>
+<% other_formats_links do |f| %>
+ <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :key => User.current.rss_key} %>
+ <%= f.link_to 'HTML', :url => {:action => 'special', :page => 'export'} %>
+<% end %>
+<% end %>
+
+<% content_for :header_tags do %>
+<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key) %>
+<% end %>
--- /dev/null
+<h2><%=l(:label_confirmation)%></h2>
+
+<div class="box"><center>
+<p><strong><%= @project.name %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p>
+
+<% form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %>
+<%= hidden_field_tag "confirm", 1 %>
+<%= submit_tag l(:button_delete) %>
+<% end %>
+</center></div>
--- /dev/null
+<div class="contextual">
+<%= link_to l(:field_summary), :action => 'index' %>
+</div>
+
+<h2><%=l(:label_workflow)%></h2>
+
+<p><%=l(:text_workflow_edit)%>:</p>
+
+<% form_tag({}, :method => 'get') do %>
+<p><label for="role_id"><%=l(:label_role)%>:</label>
+<select name="role_id">
+ <%= options_from_collection_for_select @roles, "id", "name", (@role.id unless @role.nil?) %>
+</select>
+
+<label for="tracker_id"><%=l(:label_tracker)%>:</label>
+<select name="tracker_id">
+ <%= options_from_collection_for_select @trackers, "id", "name", (@tracker.id unless @tracker.nil?) %>
+</select>
+<%= submit_tag l(:button_edit), :name => nil %>
+</p>
+<% end %>
+
+
+
+<% unless @tracker.nil? or @role.nil? or @statuses.empty? %>
+<% form_tag({}, :id => 'workflow_form' ) do %>
+<%= hidden_field_tag 'tracker_id', @tracker.id %>
+<%= hidden_field_tag 'role_id', @role.id %>
+<table class="list">
+<thead>
+ <tr>
+ <th align="left"><%=l(:label_current_status)%></th>
+ <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
+ </tr>
+ <tr>
+ <td></td>
+ <% for new_status in @statuses %>
+ <td width="<%= 75 / @statuses.size %>%" align="center"><%= new_status.name %></td>
+ <% end %>
+ </tr>
+</thead>
+<tbody>
+ <% for old_status in @statuses %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td><%= old_status.name %></td>
+ <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
+ <% for new_status in @statuses -%>
+ <td align="center">
+ <input type="checkbox"
+ name="issue_status[<%= old_status.id %>][]"
+ value="<%= new_status.id %>"
+ <%= 'checked="checked"' if new_status_ids_allowed.include? new_status.id %> />
+ </td>
+ <% end -%>
+ </tr>
+ <% end %>
+</tbody>
+</table>
+<p><%= check_all_links 'workflow_form' %></p>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
+
+<% end %>
+
+<% html_title(l(:label_workflow)) -%>
--- /dev/null
+<h2><%=l(:label_workflow)%></h2>
+
+<% if @workflow_counts.empty? %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% else %>
+<table class="list">
+<thead>
+ <tr>
+ <th></th>
+ <% @workflow_counts.first.last.each do |role, count| %>
+ <th>
+ <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
+ </th>
+
+ <% end %>
+ </tr>
+</thead>
+<tbody>
+<% @workflow_counts.each do |tracker, roles| -%>
+<tr class="<%= cycle('odd', 'even') %>">
+ <td><%= h tracker %></td>
+ <% roles.each do |role, count| -%>
+ <td align="center">
+ <%= link_to((count > 1 ? count : image_tag('false.png')), {:action => 'edit', :role_id => role, :tracker_id => tracker}, :title => l(:button_edit)) %>
+ </td>
+ <% end -%>
+</tr>
+<% end -%>
+</tbody>
+</table>
+<% end %>
--- /dev/null
+# Copy this file to additional_environment.rb and add any statements
+# that need to be passed to the Rails::Initializer. `config` is
+# available in this context.
+#
+# Example:
+#
+# config.log_level = :debug
+# config.gem "example_plugin", :lib => false
+# config.gem "timesheet_plugin", :lib => false, :version => '0.5.0'
+# config.gem "aws-s3", :lib => "aws/s3"
+# ...
+#
+
--- /dev/null
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+ class << self
+ def boot!
+ unless booted?
+ preinitialize
+ pick_boot.run
+ end
+ end
+
+ def booted?
+ defined? Rails::Initializer
+ end
+
+ def pick_boot
+ (vendor_rails? ? VendorBoot : GemBoot).new
+ end
+
+ def vendor_rails?
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
+ end
+
+ def preinitialize
+ load(preinitializer_path) if File.exist?(preinitializer_path)
+ end
+
+ def preinitializer_path
+ "#{RAILS_ROOT}/config/preinitializer.rb"
+ end
+ end
+
+ class Boot
+ def run
+ load_initializer
+ Rails::Initializer.run(:set_load_path)
+ end
+ end
+
+ class VendorBoot < Boot
+ def load_initializer
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+ Rails::Initializer.run(:install_gem_spec_stubs)
+ Rails::GemDependency.add_frozen_gem_path
+ end
+ end
+
+ class GemBoot < Boot
+ def load_initializer
+ self.class.load_rubygems
+ load_rails_gem
+ require 'initializer'
+ end
+
+ def load_rails_gem
+ if version = self.class.gem_version
+ gem 'rails', version
+ else
+ gem 'rails'
+ end
+ rescue Gem::LoadError => load_error
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+ exit 1
+ end
+
+ class << self
+ def rubygems_version
+ Gem::RubyGemsVersion rescue nil
+ end
+
+ def gem_version
+ if defined? RAILS_GEM_VERSION
+ RAILS_GEM_VERSION
+ elsif ENV.include?('RAILS_GEM_VERSION')
+ ENV['RAILS_GEM_VERSION']
+ else
+ parse_gem_version(read_environment_rb)
+ end
+ end
+
+ def load_rubygems
+ min_version = '1.3.2'
+ require 'rubygems'
+ unless rubygems_version >= min_version
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+ exit 1
+ end
+
+ rescue LoadError
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+ exit 1
+ end
+
+ def parse_gem_version(text)
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+ end
+
+ private
+ def read_environment_rb
+ File.read("#{RAILS_ROOT}/config/environment.rb")
+ end
+ end
+ end
+end
+
+# All that for this:
+Rails.boot!
--- /dev/null
+# MySQL (default setup). Versions 4.1 and 5.0 are recommended.\r
+#\r
+# Get the fast C bindings:\r
+# gem install mysql\r
+# (on OS X: gem install mysql -- --include=/usr/local/lib)\r
+# And be sure to use new-style password hashing:\r
+# http://dev.mysql.com/doc/refman/5.0/en/old-client.html\r
+\r
+production:\r
+ adapter: mysql\r
+ database: redmine\r
+ host: localhost\r
+ username: root\r
+ password:\r
+ encoding: utf8\r
+ \r
+development:\r
+ adapter: mysql\r
+ database: redmine_development\r
+ host: localhost\r
+ username: root\r
+ password:\r
+ encoding: utf8\r
+\r
+test:\r
+ adapter: mysql\r
+ database: redmine_test\r
+ host: localhost\r
+ username: root\r
+ password:\r
+ encoding: utf8\r
+\r
+test_pgsql:\r
+ adapter: postgresql\r
+ database: redmine_test\r
+ host: localhost\r
+ username: postgres\r
+ password: "postgres"\r
+\r
+test_sqlite3:\r
+ adapter: sqlite3\r
+ database: db/test.db\r
--- /dev/null
+# Outgoing email settings\r
+\r
+production:\r
+ delivery_method: :smtp\r
+ smtp_settings:\r
+ address: smtp.example.net\r
+ port: 25\r
+ domain: example.net\r
+ authentication: :login\r
+ user_name: "redmine@example.net"\r
+ password: "redmine"\r
+ \r
+development:\r
+ delivery_method: :smtp\r
+ smtp_settings:\r
+ address: 127.0.0.1\r
+ port: 25\r
+ domain: example.net\r
+ authentication: :login\r
+ user_name: "redmine@example.net"\r
+ password: "redmine"\r
--- /dev/null
+# Be sure to restart your web server when you modify this file.
+
+# Uncomment below to force Rails into production mode when
+# you don't control web/app server and can't set it the proper way
+# ENV['RAILS_ENV'] ||= 'production'
+
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+# Load Engine plugin if available
+begin
+ require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
+rescue LoadError
+ # Not available
+end
+
+Rails::Initializer.run do |config|
+ # Settings in config/environments/* take precedence those specified here
+
+ # Skip frameworks you're not going to use
+ # config.frameworks -= [ :action_web_service, :action_mailer ]
+
+ # Add additional load paths for sweepers
+ config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
+
+ # Force all environments to use the same logger level
+ # (by default production uses :info, the others :debug)
+ # config.log_level = :debug
+
+ # Enable page/fragment caching by setting a file-based store
+ # (remember to create the caching directory and make it readable to the application)
+ # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
+
+ # Activate observers that should always be running
+ # config.active_record.observers = :cacher, :garbage_collector
+ config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer
+
+ # Make Active Record use UTC-base instead of local time
+ # config.active_record.default_timezone = :utc
+
+ # Use Active Record's schema dumper instead of SQL when creating the test database
+ # (enables use of different database adapters for development and test environments)
+ # config.active_record.schema_format = :ruby
+
+ # Deliveries are disabled by default. Do NOT modify this section.
+ # Define your email configuration in email.yml instead.
+ # It will automatically turn deliveries on
+ config.action_mailer.perform_deliveries = false
+
+ # Load any local configuration that is kept out of source control
+ # (e.g. gems, patches).
+ if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
+ instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
+ end
+end
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+config.log_level = :info
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable mail delivery
+config.action_mailer.perform_deliveries = false
+config.action_mailer.raise_delivery_errors = false
+
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# In the development environment your application's code is reloaded on
+# every request. This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+config.cache_classes = false
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable delivery errors if you bad email addresses should just be ignored
+config.action_mailer.raise_delivery_errors = false
+
+# No email in production log
+config.action_mailer.logger = nil
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+config.action_mailer.perform_deliveries = true
+config.action_mailer.delivery_method = :test
+
+config.action_controller.session = {
+ :session_key => "_test_session",
+ :secret => "some secret phrase for the tests."
+}
+
+# Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
+config.action_controller.allow_forgery_protection = false
+
+config.gem "thoughtbot-shoulda", :lib => "shoulda", :source => "http://gems.github.com"
+config.gem "nofxx-object_daddy", :lib => "object_daddy", :source => "http://gems.github.com"
+config.gem "mocha"
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+config.action_mailer.perform_deliveries = true
+config.action_mailer.delivery_method = :test
+
+config.action_controller.session = {
+ :session_key => "_test_session",
+ :secret => "some secret phrase for the tests."
+}
+
+# Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
+config.action_controller.allow_forgery_protection = false
+
+config.gem "thoughtbot-shoulda", :lib => "shoulda", :source => "http://gems.github.com"
+config.gem "nofxx-object_daddy", :lib => "object_daddy", :source => "http://gems.github.com"
+config.gem "mocha"
--- /dev/null
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+config.action_mailer.perform_deliveries = true
+config.action_mailer.delivery_method = :test
+
+config.action_controller.session = {
+ :session_key => "_test_session",
+ :secret => "some secret phrase for the tests."
+}
+
+# Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
+config.action_controller.allow_forgery_protection = false
+
+config.gem "thoughtbot-shoulda", :lib => "shoulda", :source => "http://gems.github.com"
+config.gem "nofxx-object_daddy", :lib => "object_daddy", :source => "http://gems.github.com"
+config.gem "mocha"
--- /dev/null
+
+require 'activerecord'
+
+module ActiveRecord
+ class Base
+ include Redmine::I18n
+
+ # Translate attribute names for validation errors display
+ def self.human_attribute_name(attr)
+ l("field_#{attr.to_s.gsub(/_id$/, '')}")
+ end
+ end
+end
+
+module ActiveRecord
+ class Errors
+ def full_messages(options = {})
+ full_messages = []
+
+ @errors.each_key do |attr|
+ @errors[attr].each do |message|
+ next unless message
+
+ if attr == "base"
+ full_messages << message
+ elsif attr == "custom_values"
+ # Replace the generic "custom values is invalid"
+ # with the errors on custom values
+ @base.custom_values.each do |value|
+ value.errors.each do |attr, msg|
+ full_messages << value.custom_field.name + ' ' + msg
+ end
+ end
+ else
+ attr_name = @base.class.human_attribute_name(attr)
+ full_messages << attr_name + ' ' + message.to_s
+ end
+ end
+ end
+ full_messages
+ end
+ end
+end
+
+module ActionView
+ module Helpers
+ module DateHelper
+ # distance_of_time_in_words breaks when difference is greater than 30 years
+ def distance_of_date_in_words(from_date, to_date = 0, options = {})
+ from_date = from_date.to_date if from_date.respond_to?(:to_date)
+ to_date = to_date.to_date if to_date.respond_to?(:to_date)
+ distance_in_days = (to_date - from_date).abs
+
+ I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
+ case distance_in_days
+ when 0..60 then locale.t :x_days, :count => distance_in_days.round
+ when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
+ else locale.t :over_x_years, :count => (distance_in_days / 365).floor
+ end
+ end
+ end
+ end
+ end
+end
+
+ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
+
+# Adds :async_smtp and :async_sendmail delivery methods
+# to perform email deliveries asynchronously
+module AsynchronousMailer
+ %w(smtp sendmail).each do |type|
+ define_method("perform_delivery_async_#{type}") do |mail|
+ Thread.start do
+ send "perform_delivery_#{type}", mail
+ end
+ end
+ end
+end
+
+ActionMailer::Base.send :include, AsynchronousMailer
--- /dev/null
+# Add new mime types for use in respond_to blocks:
+
+Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
+Mime::Type.register 'application/pdf', :pdf
+Mime::Type.register 'image/png', :png
--- /dev/null
+I18n.default_locale = 'en'
+
+require 'redmine'
--- /dev/null
+# Loads action_mailer settings from email.yml
+# and turns deliveries on if configuration file is found
+
+filename = File.join(File.dirname(__FILE__), '..', 'email.yml')
+if File.file?(filename)
+ mailconfig = YAML::load_file(filename)
+
+ if mailconfig.is_a?(Hash) && mailconfig.has_key?(Rails.env)
+ # Enable deliveries
+ ActionMailer::Base.perform_deliveries = true
+
+ mailconfig[Rails.env].each do |k, v|
+ v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
+ ActionMailer::Base.send("#{k}=", v)
+ end
+ end
+end
--- /dev/null
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying do debug a problem that might steem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
\ No newline at end of file
--- /dev/null
+# Copyright (c) 2009 Michael Koziarski <michael@koziarski.com>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+require 'bigdecimal'
+
+alias BigDecimalUnsafe BigDecimal
+
+
+# This fixes CVE-2009-1904 however it removes legitimate functionality that your
+# application may depend on. You are *strongly* advised to upgrade your ruby
+# rather than relying on this fix for an extended period of time.
+
+def BigDecimal(initial, digits=0)
+ if initial.size > 255 || initial =~ /e/i
+ raise "Invalid big Decimal Value"
+ end
+ BigDecimalUnsafe(initial, digits)
+end
+
--- /dev/null
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
--- /dev/null
+bg:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: 'MB'
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "не съществува в списъка"
+ exclusion: "е запазено"
+ invalid: "е невалидно"
+ confirmation: "липсва одобрение"
+ accepted: "трябва да се приеме"
+ empty: "не може да е празно"
+ blank: "не може да е празно"
+ too_long: "е прекалено дълго"
+ too_short: "е прекалено късо"
+ wrong_length: "е с грешна дължина"
+ taken: "вече съществува"
+ not_a_number: "не е число"
+ not_a_date: "е невалидна дата"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ greater_than_start_date: "трябва да е след началната дата"
+ not_same_project: "не е от същия проект"
+ circular_dependency: "Тази релация ще доведе до безкрайна зависимост"
+
+ actionview_instancetag_blank_option: Изберете
+
+ general_text_No: 'Не'
+ general_text_Yes: 'Да'
+ general_text_no: 'не'
+ general_text_yes: 'да'
+ general_lang_name: 'Bulgarian'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Профилът е обновен успешно.
+ notice_account_invalid_creditentials: Невалиден потребител или парола.
+ notice_account_password_updated: Паролата е успешно променена.
+ notice_account_wrong_password: Грешна парола
+ notice_account_register_done: Профилът е създаден успешно.
+ notice_account_unknown_email: Непознат e-mail.
+ notice_can_t_change_password: Този профил е с външен метод за оторизация. Невъзможна смяна на паролата.
+ notice_account_lost_email_sent: Изпратен ви е e-mail с инструкции за избор на нова парола.
+ 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-mail на {{value}}"
+ notice_email_error: "Грешка при изпращане на e-mail ({{value}})"
+ notice_feeds_access_key_reseted: Вашия ключ за RSS достъп беше променен.
+
+ error_scm_not_found: Несъществуващ обект в хранилището.
+ error_scm_command_failed: "Грешка при опит за комуникация с хранилище: {{value}}"
+
+ mail_subject_lost_password: "Вашата парола ({{value}})"
+ mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:'
+ mail_subject_register: "Активация на профил ({{value}})"
+ mail_body_register: 'За да активирате профила си използвайте следния линк:'
+
+ gui_validation_error: 1 грешка
+ gui_validation_error_plural: "{{count}} грешки"
+
+ field_name: Име
+ field_description: Описание
+ field_summary: Групиран изглед
+ field_is_required: Задължително
+ field_firstname: Име
+ field_lastname: Фамилия
+ field_mail: Email
+ field_filename: Файл
+ field_filesize: Големина
+ field_downloads: Downloads
+ field_author: Автор
+ field_created_on: От дата
+ field_updated_on: Обновена
+ field_field_format: Тип
+ field_is_for_all: За всички проекти
+ field_possible_values: Възможни стойности
+ field_regexp: Регулярен израз
+ field_min_length: Мин. дължина
+ field_max_length: Макс. дължина
+ field_value: Стойност
+ field_category: Категория
+ field_title: Заглавие
+ field_project: Проект
+ field_issue: Задача
+ field_status: Статус
+ field_notes: Бележка
+ field_is_closed: Затворена задача
+ field_is_default: Статус по подразбиране
+ field_tracker: Тракер
+ field_subject: Относно
+ field_due_date: Крайна дата
+ field_assigned_to: Възложена на
+ field_priority: Приоритет
+ field_fixed_version: Планувана версия
+ field_user: Потребител
+ field_role: Роля
+ field_homepage: Начална страница
+ field_is_public: Публичен
+ field_parent: Подпроект на
+ field_is_in_chlog: Да се вижда ли в Изменения
+ field_is_in_roadmap: Да се вижда ли в Пътна карта
+ field_login: Потребител
+ field_mail_notification: Известия по пощата
+ field_admin: Администратор
+ field_last_login_on: Последно свързване
+ field_language: Език
+ field_effective_date: Дата
+ field_password: Парола
+ field_new_password: Нова парола
+ field_password_confirmation: Потвърждение
+ field_version: Версия
+ field_type: Тип
+ field_host: Хост
+ field_port: Порт
+ field_account: Профил
+ field_base_dn: Base DN
+ field_attr_login: Login attribute
+ field_attr_firstname: Firstname attribute
+ field_attr_lastname: Lastname attribute
+ field_attr_mail: Email attribute
+ field_onthefly: Динамично създаване на потребител
+ field_start_date: Начална дата
+ field_done_ratio: % Прогрес
+ field_auth_source: Начин на оторизация
+ field_hide_mail: Скрий e-mail адреса ми
+ field_comments: Коментар
+ field_url: Адрес
+ field_start_page: Начална страница
+ field_subproject: Подпроект
+ field_hours: Часове
+ field_activity: Дейност
+ field_spent_on: Дата
+ field_identifier: Идентификатор
+ field_is_filter: Използва се за филтър
+ field_issue_to: Свързана задача
+ field_delay: Отместване
+ field_assignable: Възможно е възлагане на задачи за тази роля
+ field_redirect_existing_links: Пренасочване на съществуващи линкове
+ field_estimated_hours: Изчислено време
+ field_default_value: Стойност по подразбиране
+
+ 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: E-mail адрес за емисии
+ setting_host_name: Хост
+ setting_text_formatting: Форматиране на текста
+ setting_wiki_compression: Wiki компресиране на историята
+ setting_feeds_limit: Лимит на Feeds
+ setting_autofetch_changesets: Автоматично обработване на ревизиите
+ setting_sys_api_enabled: Разрешаване на WS за управление
+ setting_commit_ref_keywords: Отбелязващи ключови думи
+ setting_commit_fix_keywords: Приключващи ключови думи
+ setting_autologin: Автоматичен вход
+ setting_date_format: Формат на датата
+ setting_cross_project_issue_relations: Релации на задачи между проекти
+
+ label_user: Потребител
+ label_user_plural: Потребители
+ label_user_new: Нов потребител
+ label_project: Проект
+ label_project_new: Нов проект
+ label_project_plural: Проекти
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Всички проекти
+ label_project_latest: Последни проекти
+ label_issue: Задача
+ label_issue_new: Нова задача
+ label_issue_plural: Задачи
+ label_issue_view_all: Всички задачи
+ label_document: Документ
+ label_document_new: Нов документ
+ label_document_plural: Документи
+ label_role: Роля
+ label_role_plural: Роли
+ label_role_new: Нова роля
+ label_role_and_permissions: Роли и права
+ label_member: Член
+ label_member_new: Нов член
+ label_member_plural: Членове
+ label_tracker: Тракер
+ label_tracker_plural: Тракери
+ label_tracker_new: Нов тракер
+ label_workflow: Работен процес
+ label_issue_status: Статус на задача
+ label_issue_status_plural: Статуси на задачи
+ label_issue_status_new: Нов статус
+ label_issue_category: Категория задача
+ label_issue_category_plural: Категории задачи
+ label_issue_category_new: Нова категория
+ label_custom_field: Потребителско поле
+ label_custom_field_plural: Потребителски полета
+ label_custom_field_new: Ново потребителско поле
+ label_enumerations: Списъци
+ label_enumeration_new: Нова стойност
+ label_information: Информация
+ label_information_plural: Информация
+ label_please_login: Вход
+ label_register: Регистрация
+ label_password_lost: Забравена парола
+ label_home: Начало
+ label_my_page: Лична страница
+ label_my_account: Профил
+ label_my_projects: Проекти, в които участвам
+ label_administration: Администрация
+ label_login: Вход
+ label_logout: Изход
+ label_help: Помощ
+ label_reported_issues: Публикувани задачи
+ label_assigned_to_me_issues: Възложени на мен
+ label_last_login: Последно свързване
+ label_registered_on: Регистрация
+ label_activity: Дейност
+ label_new: Нов
+ label_logged_as: Логнат като
+ label_environment: Среда
+ label_authentication: Оторизация
+ label_auth_source: Начин на оторозация
+ label_auth_source_new: Нов начин на оторизация
+ label_auth_source_plural: Начини на оторизация
+ label_subproject_plural: Подпроекти
+ label_min_max_length: Мин. - Макс. дължина
+ label_list: Списък
+ label_date: Дата
+ label_integer: Целочислен
+ label_boolean: Чекбокс
+ label_string: Текст
+ label_text: Дълъг текст
+ label_attribute: Атрибут
+ label_attribute_plural: Атрибути
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Няма изходни данни
+ label_change_status: Промяна на статуса
+ label_history: История
+ label_attachment: Файл
+ label_attachment_new: Нов файл
+ label_attachment_delete: Изтриване
+ label_attachment_plural: Файлове
+ label_report: Справка
+ label_report_plural: Справки
+ label_news: Новини
+ label_news_new: Добави
+ label_news_plural: Новини
+ label_news_latest: Последни новини
+ label_news_view_all: Виж всички
+ label_change_log: Изменения
+ label_settings: Настройки
+ label_overview: Общ изглед
+ label_version: Версия
+ label_version_new: Нова версия
+ label_version_plural: Версии
+ label_confirmation: Одобрение
+ label_export_to: Експорт към
+ label_read: Read...
+ label_public_projects: Публични проекти
+ label_open_issues: отворена
+ label_open_issues_plural: отворени
+ label_closed_issues: затворена
+ label_closed_issues_plural: затворени
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Общо
+ label_permissions: Права
+ label_current_status: Текущ статус
+ label_new_statuses_allowed: Позволени статуси
+ label_all: всички
+ label_none: никакви
+ label_next: Следващ
+ label_previous: Предишен
+ label_used_by: Използва се от
+ label_details: Детайли
+ label_add_note: Добавяне на бележка
+ label_per_page: На страница
+ label_calendar: Календар
+ label_months_from: месеца от
+ label_gantt: Gantt
+ label_internal: Вътрешен
+ label_last_changes: "последни {{count}} промени"
+ label_change_view_all: Виж всички промени
+ label_personalize_page: Персонализиране
+ label_comment: Коментар
+ label_comment_plural: Коментари
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Добавяне на коментар
+ label_comment_added: Добавен коментар
+ label_comment_delete: Изтриване на коментари
+ label_query: Потребителска справка
+ label_query_plural: Потребителски справки
+ label_query_new: Нова заявка
+ label_filter_add: Добави филтър
+ label_filter_plural: Филтри
+ label_equals: е
+ label_not_equals: не е
+ label_in_less_than: след по-малко от
+ label_in_more_than: след повече от
+ label_in: в следващите
+ label_today: днес
+ label_this_week: тази седмица
+ label_less_than_ago: преди по-малко от
+ label_more_than_ago: преди повече от
+ label_ago: преди
+ label_contains: съдържа
+ label_not_contains: не съдържа
+ label_day_plural: дни
+ label_repository: Хранилище
+ label_browse: Разглеждане
+ label_modification: "{{count}} промяна"
+ label_modification_plural: "{{count}} промени"
+ label_revision: Ревизия
+ label_revision_plural: Ревизии
+ label_added: добавено
+ label_modified: променено
+ label_deleted: изтрито
+ label_latest_revision: Последна ревизия
+ label_latest_revision_plural: Последни ревизии
+ label_view_revisions: Виж ревизиите
+ label_max_size: Максимална големина
+ label_sort_highest: Премести най-горе
+ label_sort_higher: Премести по-горе
+ label_sort_lower: Премести по-долу
+ label_sort_lowest: Премести най-долу
+ label_roadmap: Пътна карта
+ label_roadmap_due_in: "Излиза след {{value}}"
+ label_roadmap_overdue: "{{value}} закъснение"
+ label_roadmap_no_issues: Няма задачи за тази версия
+ label_search: Търсене
+ label_result_plural: Pезултати
+ label_all_words: Всички думи
+ label_wiki: Wiki
+ label_wiki_edit: Wiki редакция
+ label_wiki_edit_plural: Wiki редакции
+ label_wiki_page: Wiki page
+ label_wiki_page_plural: Wiki pages
+ label_index_by_title: Индекс
+ label_index_by_date: Индекс по дата
+ label_current_version: Текуща версия
+ label_preview: Преглед
+ label_feed_plural: Feeds
+ label_changes_details: Подробни промени
+ label_issue_tracking: Тракинг
+ label_spent_time: Отделено време
+ label_f_hour: "{{value}} час"
+ label_f_hour_plural: "{{value}} часа"
+ label_time_tracking: Отделяне на време
+ label_change_plural: Промени
+ label_statistics: Статистики
+ label_commits_per_month: Ревизии по месеци
+ label_commits_per_author: Ревизии по автор
+ label_view_diff: Виж разликите
+ label_diff_inline: хоризонтално
+ label_diff_side_by_side: вертикално
+ label_options: Опции
+ label_copy_workflow_from: Копирай работния процес от
+ label_permissions_report: Справка за права
+ label_watched_issues: Наблюдавани задачи
+ label_related_issues: Свързани задачи
+ label_applied_status: Промени статуса на
+ label_loading: Зареждане...
+ label_relation_new: Нова релация
+ label_relation_delete: Изтриване на релация
+ label_relates_to: свързана със
+ label_duplicates: дублира
+ label_blocks: блокира
+ label_blocked_by: блокирана от
+ label_precedes: предшества
+ label_follows: изпълнява се след
+ label_end_to_start: end to start
+ label_end_to_end: end to end
+ label_start_to_start: start to start
+ label_start_to_end: start to end
+ label_stay_logged_in: Запомни ме
+ label_disabled: забранено
+ label_show_completed_versions: Показване на реализирани версии
+ label_me: аз
+ label_board: Форум
+ label_board_new: Нов форум
+ label_board_plural: Форуми
+ label_topic_plural: Теми
+ label_message_plural: Съобщения
+ label_message_last: Последно съобщение
+ label_message_new: Нова тема
+ label_reply_plural: Отговори
+ label_send_information: Изпращане на информацията до потребителя
+ label_year: Година
+ label_month: Месец
+ label_week: Седмица
+ label_date_from: От
+ label_date_to: До
+ label_language_based: В зависимост от езика
+ label_sort_by: "Сортиране по {{value}}"
+ label_send_test_email: Изпращане на тестов e-mail
+ label_feeds_access_key_created_on: "{{value}} от създаването на RSS ключа"
+ label_module_plural: Модули
+ label_added_time_by: "Публикувана от {{author}} преди {{age}}"
+ label_updated_time: "Обновена преди {{value}}"
+ label_jump_to_a_project: Проект...
+
+ button_login: Вход
+ button_submit: Прикачване
+ button_save: Запис
+ button_check_all: Избор на всички
+ button_uncheck_all: Изчистване на всички
+ button_delete: Изтриване
+ button_create: Създаване
+ button_test: Тест
+ button_edit: Редакция
+ button_add: Добавяне
+ button_change: Промяна
+ button_apply: Приложи
+ button_clear: Изчисти
+ button_lock: Заключване
+ button_unlock: Отключване
+ button_download: Download
+ button_list: Списък
+ button_view: Преглед
+ button_move: Преместване
+ button_back: Назад
+ button_cancel: Отказ
+ button_activate: Активация
+ button_sort: Сортиране
+ button_log_time: Отделяне на време
+ button_rollback: Върни се към тази ревизия
+ button_watch: Наблюдавай
+ button_unwatch: Спри наблюдението
+ button_reply: Отговор
+ button_archive: Архивиране
+ button_unarchive: Разархивиране
+ button_reset: Генериране наново
+ button_rename: Преименуване
+
+ status_active: активен
+ status_registered: регистриран
+ status_locked: заключен
+
+ text_select_mail_notifications: Изберете събития за изпращане на e-mail.
+ text_regexp_info: пр. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 - без ограничения
+ text_project_destroy_confirmation: Сигурни ли сте, че искате да изтриете проекта и данните в него?
+ text_workflow_edit: Изберете роля и тракер за да редактирате работния процес
+ text_are_you_sure: Сигурни ли сте?
+ text_tip_task_begin_day: задача започваща този ден
+ text_tip_task_end_day: задача завършваща този ден
+ text_tip_task_begin_end_day: задача започваща и завършваща този ден
+ text_project_identifier_info: 'Позволени са малки букви (a-z), цифри и тирета.<br />Невъзможна промяна след запис.'
+ text_caracters_maximum: "До {{count}} символа."
+ text_length_between: "От {{min}} до {{max}} символа."
+ text_tracker_no_workflow: Няма дефиниран работен процес за този тракер
+ text_unallowed_characters: Непозволени символи
+ text_comma_separated: Позволено е изброяване (с разделител запетая).
+ text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от ревизии
+ text_issue_added: "Публикувана е нова задача с номер {{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: Преобвързване с категория
+
+ default_role_manager: Мениджър
+ default_role_developper: Разработчик
+ default_role_reporter: Публикуващ
+ default_tracker_bug: Бъг
+ default_tracker_feature: Функционалност
+ default_tracker_support: Поддръжка
+ default_issue_status_new: Нова
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Приключена
+ default_issue_status_feedback: Обратна връзка
+ default_issue_status_closed: Затворена
+ default_issue_status_rejected: Отхвърлена
+ default_doc_category_user: Документация за потребителя
+ default_doc_category_tech: Техническа документация
+ default_priority_low: Нисък
+ default_priority_normal: Нормален
+ default_priority_high: Висок
+ default_priority_urgent: Спешен
+ default_priority_immediate: Веднага
+ default_activity_design: Дизайн
+ default_activity_development: Разработка
+
+ enumeration_issue_priorities: Приоритети на задачи
+ enumeration_doc_categories: Категории документи
+ enumeration_activities: Дейности (time tracking)
+ label_file_plural: Файлове
+ label_changeset_plural: Ревизии
+ field_column_names: Колони
+ label_default_columns: По подразбиране
+ setting_issue_list_default_columns: Показвани колони по подразбиране
+ setting_repositories_encodings: Кодови таблици
+ notice_no_issue_selected: "Няма избрани задачи."
+ label_bulk_edit_selected_issues: Редактиране на задачи
+ label_no_change_option: (Без промяна)
+ notice_failed_to_save_issues: "Неуспешен запис на {{count}} задачи от {{total}} избрани: {{ids}}."
+ label_theme: Тема
+ label_default: По подразбиране
+ label_search_titles_only: Само в заглавията
+ label_nobody: никой
+ button_change_password: Промяна на парола
+ text_user_mail_option: "За неизбраните проекти, ще получавате известия само за наблюдавани дейности или в които участвате (т.е. автор или назначени на мен)."
+ label_user_mail_option_selected: "За всички събития само в избраните проекти..."
+ label_user_mail_option_all: "За всяко събитие в проектите, в които участвам"
+ label_user_mail_option_none: "Само за наблюдавани или в които участвам (автор или назначени на мен)"
+ setting_emails_footer: Подтекст за e-mail
+ label_float: Дробно
+ button_copy: Копиране
+ mail_body_account_information_external: "Можете да използвате вашия {{value}} профил за вход."
+ mail_body_account_information: Информацията за профила ви
+ setting_protocol: Протокол
+ label_user_mail_no_self_notified: "Не искам известия за извършени от мен промени"
+ setting_time_format: Формат на часа
+ label_registration_activation_by_email: активиране на профила по email
+ mail_subject_account_activation_request: "Заявка за активиране на профил в {{value}}"
+ mail_body_account_activation_request: "Има новорегистриран потребител ({{value}}), очакващ вашето одобрение:"
+ label_registration_automatic_activation: автоматично активиране
+ label_registration_manual_activation: ръчно активиране
+ notice_account_pending: "Профилът Ви е създаден и очаква одобрение от администратор."
+ field_time_zone: Часова зона
+ text_caracters_minimum: "Минимум {{count}} символа."
+ setting_bcc_recipients: Получатели на скрито копие (bcc)
+ button_annotate: Анотация
+ label_issues_by: "Задачи по {{value}}"
+ field_searchable: С възможност за търсене
+ label_display_per_page: "На страница по: {{value}}"
+ setting_per_page_options: Опции за страниране
+ label_age: Възраст
+ notice_default_data_loaded: Примерната информацията е успешно заредена.
+ text_load_default_configuration: Зареждане на примерна информация
+ text_no_configuration_data: "Все още не са конфигурирани Роли, тракери, статуси на задачи и работен процес.\nСтрого се препоръчва зареждането на примерната информация. Веднъж заредена ще имате възможност да я редактирате."
+ error_can_t_load_default_data: "Грешка при зареждане на примерната информация: {{value}}"
+ button_update: Обновяване
+ label_change_properties: Промяна на настройки
+ label_general: Основни
+ label_repository_plural: Хранилища
+ label_associated_revisions: Асоциирани ревизии
+ setting_user_format: Потребителски формат
+ text_status_changed_by_changeset: "Приложено с ревизия {{value}}."
+ label_more: Още
+ text_issues_destroy_confirmation: 'Сигурни ли сте, че искате да изтриете избраните задачи?'
+ label_scm: SCM (Система за контрол на кода)
+ text_select_project_modules: 'Изберете активните модули за този проект:'
+ label_issue_added: Добавена задача
+ label_issue_updated: Обновена задача
+ label_document_added: Добавен документ
+ label_message_posted: Добавено съобщение
+ label_file_added: Добавен файл
+ label_news_added: Добавена новина
+ project_module_boards: Форуми
+ project_module_issue_tracking: Тракинг
+ project_module_wiki: Wiki
+ project_module_files: Файлове
+ project_module_documents: Документи
+ project_module_repository: Хранилище
+ project_module_news: Новини
+ project_module_time_tracking: Отделяне на време
+ text_file_repository_writable: Възможност за писане в хранилището с файлове
+ text_default_administrator_account_changed: Сменен фабричния администраторски профил
+ text_rmagick_available: Наличен RMagick (по избор)
+ button_configure: Конфигуриране
+ label_plugins: Плъгини
+ label_ldap_authentication: LDAP оторизация
+ label_downloads_abbr: D/L
+ label_this_month: текущия месец
+ label_last_n_days: "последните {{count}} дни"
+ label_all_time: всички
+ label_this_year: текущата година
+ label_date_range: Период
+ label_last_week: последната седмица
+ label_yesterday: вчера
+ label_last_month: последния месец
+ label_add_another_file: Добавяне на друг файл
+ label_optional_description: Незадължително описание
+ text_destroy_time_entries_question: "{{hours}} часа са отделени на задачите, които искате да изтриете. Какво избирате?"
+ error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект'
+ text_assign_time_entries_to_project: Прехвърляне на отделеното време към проект
+ text_destroy_time_entries: Изтриване на отделеното време
+ text_reassign_time_entries: 'Прехвърляне на отделеното време към задача:'
+ setting_activity_days_default: Брой дни показвани на таб Дейност
+ label_chronological_order: Хронологичен ред
+ field_comments_sorting: Сортиране на коментарите
+ label_reverse_chronological_order: Обратен хронологичен ред
+ label_preferences: Предпочитания
+ setting_display_subprojects_issues: Показване на подпроектите в проектите по подразбиране
+ label_overall_activity: Цялостна дейност
+ setting_default_projects_public: Новите проекти са публични по подразбиране
+ error_scm_annotate: "Обектът не съществува или не може да бъде анотиран."
+ label_planning: Планиране
+ text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
+ label_and_its_subprojects: "{{value}} and its subprojects"
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ text_user_wrote: "{{value}} wrote:"
+ label_duplicated_by: duplicated by
+ setting_enabled_scm: Enabled SCM
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: API key
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: Parent page
+ label_issue_watchers: Watchers
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: Quote
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: renamed
+ label_copied: copied
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: Example
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+#Ernad Husremovic hernad@bring.out.ba
+
+bs:
+ 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_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"
+
+
+ number:
+ format:
+ precision: 2
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: 'KM'
+ format: '%u %n'
+ separator:
+ delimiter:
+ precision:
+ percentage:
+ format:
+ delimiter: ""
+ precision:
+ format:
+ delimiter: ""
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ 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:
+ 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"
+
+ 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: utf8
+ general_pdf_encoding: utf8
+ 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š RSS 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 danima"
+ mail_body_reminder: "{{count}} aktivnost(i) koje su dodjeljenje vama u narednim {{days}} danima:"
+
+ gui_validation_error: 1 greška
+ gui_validation_error_plural: "{{count}} grešaka"
+
+ 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_chlog: Aktivnosti prikazane u logu promjena
+ 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 "RSS" 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_repositories_encodings: Enkodiranje repozitorija
+ setting_commit_logs_encoding: 'Enkodiranje "commit" poruka'
+ 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_manage_documents: Upravljaj dokumentima
+ 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_download: "{{count}} download"
+ label_download_plural: "{{count}} download-i"
+ 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_change_log: Log promjena
+ 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_modification: "{{count}} promjena"
+ label_modification_plural: "{{count}} promjene"
+ 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: "RSS 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_option_none: "Samo za stvari koje ja gledam ili sam u njih uključen"
+ 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_task_begin_day: zadatak počinje danas
+ text_tip_task_end_day: zadatak završava danas
+ text_tip_task_begin_end_day: zadatak započinje i završava danas
+ text_project_identifier_info: 'Samo mala slova (a-z), brojevi i crtice su dozvoljeni.<br />Nakon snimanja, identifikator se ne može mijenjati.'
+ 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/email.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_developper: 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: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+ca:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%d-%m-%Y"
+ short: "%e de %b"
+ long: "%a, %e de %b de %Y"
+
+ day_names: [Diumenge, Dilluns, Dimarts, Dimecres, Dijous, Divendres, Dissabte]
+ abbr_day_names: [dg, dl, dt, dc, dj, dv, ds]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Gener, Febrer, Març, Abril, Maig, Juny, Juliol, Agost, Setembre, Octubre, Novembre, Desembre]
+ abbr_month_names: [~, Gen, Feb, Mar, Abr, Mai, Jun, Jul, Ago, Set, Oct, Nov, Des]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%d-%m-%Y %H:%M"
+ time: "%H:%M"
+ short: "%e de %b, %H:%M"
+ long: "%a, %e de %b de %Y, %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "mig minut"
+ less_than_x_seconds:
+ one: "menys d'un segon"
+ other: "menys de {{count}} segons"
+ x_seconds:
+ one: "1 segons"
+ other: "{{count}} segons"
+ less_than_x_minutes:
+ one: "menys d'un minut"
+ other: "menys de {{count}} minuts"
+ x_minutes:
+ one: "1 minut"
+ other: "{{count}} minuts"
+ about_x_hours:
+ one: "aproximadament 1 hora"
+ other: "aproximadament {{count}} hores"
+ x_days:
+ one: "1 dia"
+ other: "{{count}} dies"
+ about_x_months:
+ one: "aproximadament 1 mes"
+ other: "aproximadament {{count}} mesos"
+ x_months:
+ one: "1 mes"
+ other: "{{count}} mesos"
+ about_x_years:
+ one: "aproximadament 1 any"
+ other: "aproximadament {{count}} anys"
+ over_x_years:
+ one: "més d'un any"
+ other: "més de {{count}} anys"
+
+ number:
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ 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:
+ messages:
+ inclusion: "no està inclòs a la llista"
+ exclusion: "està reservat"
+ invalid: "no és vàlid"
+ confirmation: "la confirmació no coincideix"
+ accepted: "s'ha d'acceptar"
+ empty: "no pot estar buit"
+ blank: "no pot estar en blanc"
+ too_long: "és massa llarg"
+ too_short: "és massa curt"
+ wrong_length: "la longitud és incorrecta"
+ taken: "ja s'està utilitzant"
+ not_a_number: "no és un número"
+ not_a_date: "no és una data vàlida"
+ greater_than: "ha de ser més gran que {{count}}"
+ greater_than_or_equal_to: "ha de ser més gran o igual a {{count}}"
+ equal_to: "ha de ser igual a {{count}}"
+ less_than: "ha de ser menys que {{count}}"
+ less_than_or_equal_to: "ha de ser menys o igual a {{count}}"
+ odd: "ha de ser senar"
+ even: "ha de ser parell"
+ greater_than_start_date: "ha de ser superior que la data inicial"
+ not_same_project: "no pertany al mateix projecte"
+ circular_dependency: "Aquesta relació crearia una dependència circular"
+
+ actionview_instancetag_blank_option: Seleccioneu
+
+ general_text_No: 'No'
+ general_text_Yes: 'Si'
+ general_text_no: 'no'
+ general_text_yes: 'si'
+ general_lang_name: 'Català'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-15
+ general_pdf_encoding: ISO-8859-15
+ general_first_day_of_week: '1'
+
+ notice_account_updated: "El compte s'ha actualitzat correctament."
+ notice_account_invalid_creditentials: Usuari o contrasenya invàlid
+ notice_account_password_updated: "La contrasenya s'ha modificat correctament."
+ notice_account_wrong_password: Contrasenya incorrecta
+ notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic."
+ notice_account_unknown_email: Usuari desconegut.
+ notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya."
+ notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova."
+ notice_account_activated: "El compte s'ha activat. Ara podeu entrar."
+ notice_successful_create: "S'ha creat correctament."
+ notice_successful_update: "S'ha modificat correctament."
+ notice_successful_delete: "S'ha suprimit correctament."
+ notice_successful_connection: "S'ha connectat correctament."
+ notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit."
+ notice_locking_conflict: Un altre usuari ha actualitzat les dades.
+ notice_not_authorized: No teniu permís per a accedir a aquesta pàgina.
+ notice_email_sent: "S'ha enviat un correu electrònic a {{value}}"
+ notice_email_error: "S'ha produït un error en enviar el correu ({{value}})"
+ notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del RSS."
+ notice_failed_to_save_issues: "No s'han pogut desar %s assumptes de {{count}} seleccionats: {{ids}}."
+ notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar."
+ notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador."
+ notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada."
+ notice_unable_delete_version: "No s'ha pogut suprimir la versió."
+
+ error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: {{value}} "
+ error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit."
+ error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: {{value}}"
+ error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar."
+ error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte"
+
+ warning_attachments_not_saved: "No s'han pogut desar {{count}} fitxers."
+
+ mail_subject_lost_password: "Contrasenya de {{value}}"
+ mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:"
+ mail_subject_register: "Activació del compte de {{value}}"
+ mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:"
+ mail_body_account_information_external: "Podeu utilitzar el compte «{{value}}» per a entrar."
+ mail_body_account_information: Informació del compte
+ mail_subject_account_activation_request: "Sol·licitud d'activació del compte de {{value}}"
+ mail_body_account_activation_request: "S'ha registrat un usuari nou ({{value}}). El seu compte està pendent d'aprovació:"
+ mail_subject_reminder: "%d assumptes venceran els següents {{count}} dies"
+ mail_body_reminder: "{{count}} assumptes que teniu assignades venceran els següents {{days}} dies:"
+
+ gui_validation_error: 1 error
+ gui_validation_error_plural: "{{count}} errors"
+
+ field_name: Nom
+ field_description: Descripció
+ field_summary: Resum
+ field_is_required: Necessari
+ field_firstname: Nom
+ field_lastname: Cognom
+ field_mail: Correu electrònic
+ field_filename: Fitxer
+ field_filesize: Mida
+ field_downloads: Baixades
+ field_author: Autor
+ field_created_on: Creat
+ field_updated_on: Actualitzat
+ field_field_format: Format
+ field_is_for_all: Per a tots els projectes
+ field_possible_values: Valores possibles
+ field_regexp: Expressió regular
+ field_min_length: Longitud mínima
+ field_max_length: Longitud màxima
+ field_value: Valor
+ field_category: Categoria
+ field_title: Títol
+ field_project: Projecte
+ field_issue: Assumpte
+ field_status: Estat
+ field_notes: Notes
+ field_is_closed: Assumpte tancat
+ field_is_default: Estat predeterminat
+ field_tracker: Seguidor
+ field_subject: Tema
+ field_due_date: Data de venciment
+ field_assigned_to: Assignat a
+ field_priority: Prioritat
+ field_fixed_version: Versió objectiu
+ field_user: Usuari
+ field_role: Rol
+ field_homepage: Pàgina web
+ field_is_public: Públic
+ field_parent: Subprojecte de
+ field_is_in_chlog: Assumptes mostrats en el registre de canvis
+ field_is_in_roadmap: Assumptes mostrats en la planificació
+ field_login: Entrada
+ field_mail_notification: Notificacions per correu electrònic
+ field_admin: Administrador
+ field_last_login_on: Última connexió
+ field_language: Idioma
+ field_effective_date: Data
+ field_password: Contrasenya
+ field_new_password: Contrasenya nova
+ field_password_confirmation: Confirmació
+ field_version: Versió
+ field_type: Tipus
+ field_host: Ordinador
+ field_port: Port
+ field_account: Compte
+ field_base_dn: Base DN
+ field_attr_login: "Atribut d'entrada"
+ field_attr_firstname: Atribut del nom
+ field_attr_lastname: Atribut del cognom
+ field_attr_mail: Atribut del correu electrònic
+ field_onthefly: "Creació de l'usuari «al vol»"
+ field_start_date: Inici
+ field_done_ratio: % realitzat
+ field_auth_source: "Mode d'autenticació"
+ field_hide_mail: "Oculta l'adreça de correu electrònic"
+ field_comments: Comentari
+ field_url: URL
+ field_start_page: Pàgina inicial
+ field_subproject: Subprojecte
+ field_hours: Hores
+ field_activity: Activitat
+ field_spent_on: Data
+ field_identifier: Identificador
+ field_is_filter: "S'ha utilitzat com a filtre"
+ field_issue_to: Assumpte relacionat
+ field_delay: Retard
+ field_assignable: Es poden assignar assumptes a aquest rol
+ field_redirect_existing_links: Redirigeix els enllaços existents
+ field_estimated_hours: Temps previst
+ field_column_names: Columnes
+ field_time_zone: Zona horària
+ field_searchable: Es pot cercar
+ field_default_value: Valor predeterminat
+ field_comments_sorting: Mostra els comentaris
+ field_parent_title: Pàgina pare
+ field_editable: Es pot editar
+ field_watcher: Vigilància
+ field_identity_url: URL OpenID
+ field_content: Contingut
+
+ setting_app_title: "Títol de l'aplicació"
+ setting_app_subtitle: "Subtítol de l'aplicació"
+ setting_welcome_text: Text de benvinguda
+ setting_default_language: Idioma predeterminat
+ setting_login_required: Es necessita autenticació
+ setting_self_registration: Registre automàtic
+ setting_attachment_max_size: Mida màxima dels adjunts
+ setting_issues_export_limit: "Límit d'exportació d'assumptes"
+ setting_mail_from: "Adreça de correu electrònic d'emissió"
+ setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc)
+ setting_plain_text_mail: només text pla (no HTML)
+ setting_host_name: "Nom de l'ordinador"
+ setting_text_formatting: Format del text
+ setting_wiki_compression: "Comprimeix l'historial del wiki"
+ setting_feeds_limit: Límit de contingut del canal
+ setting_default_projects_public: Els projectes nous són públics per defecte
+ setting_autofetch_changesets: Omple automàticament les publicacions
+ setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit
+ setting_commit_ref_keywords: Paraules claus per a la referència
+ setting_commit_fix_keywords: Paraules claus per a la correcció
+ setting_autologin: Entrada automàtica
+ setting_date_format: Format de la data
+ setting_time_format: Format de hora
+ setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes"
+ setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes"
+ setting_repositories_encodings: Codificacions del dipòsit
+ setting_commit_logs_encoding: Codificació dels missatges publicats
+ setting_emails_footer: Peu dels correus electrònics
+ setting_protocol: Protocol
+ setting_per_page_options: Opcions dels objectes per pàgina
+ setting_user_format: "Format de com mostrar l'usuari"
+ setting_activity_days_default: "Dies a mostrar l'activitat del projecte"
+ setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte"
+ setting_enabled_scm: "Habilita l'SCM"
+ setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada"
+ setting_mail_handler_api_key: Clau API
+ setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials
+ setting_gravatar_enabled: "Utilitza les icones d'usuari Gravatar"
+ setting_diff_max_lines_displayed: Número màxim de línies amb diferències mostrades
+ setting_file_max_size_displayed: Mida màxima dels fitxers de text mostrats en línia
+ setting_repository_log_display_limit: Número màxim de revisions que es mostren al registre de fitxers
+ setting_openid: "Permet entrar i registrar-se amb l'OpenID"
+
+ permission_edit_project: Edita el projecte
+ permission_select_project_modules: Selecciona els mòduls del projecte
+ permission_manage_members: Gestiona els membres
+ permission_manage_versions: Gestiona les versions
+ permission_manage_categories: Gestiona les categories dels assumptes
+ permission_add_issues: Afegeix assumptes
+ permission_edit_issues: Edita els assumptes
+ permission_manage_issue_relations: Gestiona les relacions dels assumptes
+ permission_add_issue_notes: Afegeix notes
+ permission_edit_issue_notes: Edita les notes
+ permission_edit_own_issue_notes: Edita les notes pròpies
+ permission_move_issues: Mou els assumptes
+ permission_delete_issues: Suprimeix els assumptes
+ permission_manage_public_queries: Gestiona les consultes públiques
+ permission_save_queries: Desa les consultes
+ permission_view_gantt: Visualitza la gràfica de Gantt
+ permission_view_calendar: Visualitza el calendari
+ permission_view_issue_watchers: Visualitza la llista de vigilàncies
+ permission_add_issue_watchers: Afegeix vigilàncies
+ permission_log_time: Registra el temps invertit
+ permission_view_time_entries: Visualitza el temps invertit
+ permission_edit_time_entries: Edita els registres de temps
+ permission_edit_own_time_entries: Edita els registres de temps propis
+ permission_manage_news: Gestiona les noticies
+ permission_comment_news: Comenta les noticies
+ permission_manage_documents: Gestiona els documents
+ permission_view_documents: Visualitza els documents
+ permission_manage_files: Gestiona els fitxers
+ permission_view_files: Visualitza els fitxers
+ permission_manage_wiki: Gestiona el wiki
+ permission_rename_wiki_pages: Canvia el nom de les pàgines wiki
+ permission_delete_wiki_pages: Suprimeix les pàgines wiki
+ permission_view_wiki_pages: Visualitza el wiki
+ permission_view_wiki_edits: "Visualitza l'historial del wiki"
+ permission_edit_wiki_pages: Edita les pàgines wiki
+ permission_delete_wiki_pages_attachments: Suprimeix adjunts
+ permission_protect_wiki_pages: Protegeix les pàgines wiki
+ permission_manage_repository: Gestiona el dipòsit
+ permission_browse_repository: Navega pel dipòsit
+ permission_view_changesets: Visualitza els canvis realitzats
+ permission_commit_access: Accés a les publicacions
+ permission_manage_boards: Gestiona els taulers
+ permission_view_messages: Visualitza els missatges
+ permission_add_messages: Envia missatges
+ permission_edit_messages: Edita els missatges
+ permission_edit_own_messages: Edita els missatges propis
+ permission_delete_messages: Suprimeix els missatges
+ permission_delete_own_messages: Suprimeix els missatges propis
+
+ project_module_issue_tracking: "Seguidor d'assumptes"
+ project_module_time_tracking: Seguidor de temps
+ project_module_news: Noticies
+ project_module_documents: Documents
+ project_module_files: Fitxers
+ project_module_wiki: Wiki
+ project_module_repository: Dipòsit
+ project_module_boards: Taulers
+
+ label_user: Usuari
+ label_user_plural: Usuaris
+ label_user_new: Usuari nou
+ label_project: Projecte
+ label_project_new: Projecte nou
+ label_project_plural: Projectes
+ label_x_projects:
+ zero: cap projecte
+ one: 1 projecte
+ other: "{{count}} projectes"
+ label_project_all: Tots els projectes
+ label_project_latest: Els últims projectes
+ label_issue: Assumpte
+ label_issue_new: Assumpte nou
+ label_issue_plural: Assumptes
+ label_issue_view_all: Visualitza tots els assumptes
+ label_issues_by: "Assumptes per {{value}}"
+ label_issue_added: Assumpte afegit
+ label_issue_updated: Assumpte actualitzat
+ label_document: Document
+ label_document_new: Document nou
+ label_document_plural: Documents
+ label_document_added: Document afegit
+ label_role: Rol
+ label_role_plural: Rols
+ label_role_new: Rol nou
+ label_role_and_permissions: Rols i permisos
+ label_member: Membre
+ label_member_new: Membre nou
+ label_member_plural: Membres
+ label_tracker: Seguidor
+ label_tracker_plural: Seguidors
+ label_tracker_new: Seguidor nou
+ label_workflow: Flux de treball
+ label_issue_status: "Estat de l'assumpte"
+ label_issue_status_plural: "Estats de l'assumpte"
+ label_issue_status_new: Estat nou
+ label_issue_category: "Categoria de l'assumpte"
+ label_issue_category_plural: "Categories de l'assumpte"
+ label_issue_category_new: Categoria nova
+ label_custom_field: Camp personalitzat
+ label_custom_field_plural: Camps personalitzats
+ label_custom_field_new: Camp personalitzat nou
+ label_enumerations: Enumeracions
+ label_enumeration_new: Valor nou
+ label_information: Informació
+ label_information_plural: Informació
+ label_please_login: Entreu
+ label_register: Registre
+ label_login_with_open_id_option: o entra amb l'OpenID
+ label_password_lost: Contrasenya perduda
+ label_home: Inici
+ label_my_page: La meva pàgina
+ label_my_account: El meu compte
+ label_my_projects: Els meus projectes
+ label_administration: Administració
+ label_login: Entra
+ label_logout: Surt
+ label_help: Ajuda
+ label_reported_issues: Assumptes informats
+ label_assigned_to_me_issues: Assumptes assignats a mi
+ label_last_login: Última connexió
+ label_registered_on: Informat el
+ label_activity: Activitat
+ label_overall_activity: Activitat global
+ label_user_activity: "Activitat de {{value}}"
+ label_new: Nou
+ label_logged_as: Heu entrat com a
+ label_environment: Entorn
+ label_authentication: Autenticació
+ label_auth_source: "Mode d'autenticació"
+ label_auth_source_new: "Mode d'autenticació nou"
+ label_auth_source_plural: "Modes d'autenticació"
+ label_subproject_plural: Subprojectes
+ label_and_its_subprojects: "{{value}} i els seus subprojectes"
+ label_min_max_length: Longitud mín - max
+ label_list: Llist
+ label_date: Data
+ label_integer: Enter
+ label_float: Flotant
+ label_boolean: Booleà
+ label_string: Text
+ label_text: Text llarg
+ label_attribute: Atribut
+ label_attribute_plural: Atributs
+ label_download: "{{count}} baixada"
+ label_download_plural: "{{count}} baixades"
+ label_no_data: Sense dades a mostrar
+ label_change_status: "Canvia l'estat"
+ label_history: Historial
+ label_attachment: Fitxer
+ label_attachment_new: Fitxer nou
+ label_attachment_delete: Suprimeix el fitxer
+ label_attachment_plural: Fitxers
+ label_file_added: Fitxer afegit
+ label_report: Informe
+ label_report_plural: Informes
+ label_news: Noticies
+ label_news_new: Afegeix noticies
+ label_news_plural: Noticies
+ label_news_latest: Últimes noticies
+ label_news_view_all: Visualitza totes les noticies
+ label_news_added: Noticies afegides
+ label_change_log: Registre de canvis
+ label_settings: Paràmetres
+ label_overview: Resum
+ label_version: Versió
+ label_version_new: Versió nova
+ label_version_plural: Versions
+ label_confirmation: Confirmació
+ label_export_to: 'També disponible a:'
+ label_read: Llegeix...
+ label_public_projects: Projectes públics
+ label_open_issues: obert
+ label_open_issues_plural: oberts
+ label_closed_issues: tancat
+ label_closed_issues_plural: tancats
+ label_x_open_issues_abbr_on_total:
+ zero: 0 oberts / {{total}}
+ one: 1 obert / {{total}}
+ other: "{{count}} oberts / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 oberts
+ one: 1 obert
+ other: "{{count}} oberts"
+ label_x_closed_issues_abbr:
+ zero: 0 tancats
+ one: 1 tancat
+ other: "{{count}} tancats"
+ label_total: Total
+ label_permissions: Permisos
+ label_current_status: Estat actual
+ label_new_statuses_allowed: Nous estats autoritzats
+ label_all: tots
+ label_none: cap
+ label_nobody: ningú
+ label_next: Següent
+ label_previous: Anterior
+ label_used_by: Utilitzat per
+ label_details: Detalls
+ label_add_note: Afegeix una nota
+ label_per_page: Per pàgina
+ label_calendar: Calendari
+ label_months_from: mesos des de
+ label_gantt: Gantt
+ label_internal: Intern
+ label_last_changes: "últims {{count}} canvis"
+ label_change_view_all: Visualitza tots els canvis
+ label_personalize_page: Personalitza aquesta pàgina
+ label_comment: Comentari
+ label_comment_plural: Comentaris
+ label_x_comments:
+ zero: sense comentaris
+ one: 1 comentari
+ other: "{{count}} comentaris"
+ label_comment_add: Afegeix un comentari
+ label_comment_added: Comentari afegit
+ label_comment_delete: Suprimeix comentaris
+ label_query: Consulta personalitzada
+ label_query_plural: Consultes personalitzades
+ label_query_new: Consulta nova
+ label_filter_add: Afegeix un filtre
+ label_filter_plural: Filtres
+ label_equals: és
+ label_not_equals: no és
+ label_in_less_than: en menys de
+ label_in_more_than: en més de
+ label_in: en
+ label_today: avui
+ label_all_time: tot el temps
+ label_yesterday: ahir
+ label_this_week: aquesta setmana
+ label_last_week: "l'última setmana"
+ label_last_n_days: "els últims {{count}} dies"
+ label_this_month: aquest més
+ label_last_month: "l'últim més"
+ label_this_year: aquest any
+ label_date_range: Abast de les dates
+ label_less_than_ago: fa menys de
+ label_more_than_ago: fa més de
+ label_ago: fa
+ label_contains: conté
+ label_not_contains: no conté
+ label_day_plural: dies
+ label_repository: Dipòsit
+ label_repository_plural: Dipòsits
+ label_browse: Navega
+ label_modification: "{{count}} canvi"
+ label_modification_plural: "{{count}} canvis"
+ label_revision: Revisió
+ label_revision_plural: Revisions
+ label_associated_revisions: Revisions associades
+ label_added: afegit
+ label_modified: modificat
+ label_renamed: reanomenat
+ label_copied: copiat
+ label_deleted: suprimit
+ label_latest_revision: Última revisió
+ label_latest_revision_plural: Últimes revisions
+ label_view_revisions: Visualitza les revisions
+ label_max_size: Mida màxima
+ label_sort_highest: Mou a la part superior
+ label_sort_higher: Mou cap amunt
+ label_sort_lower: Mou cap avall
+ label_sort_lowest: Mou a la part inferior
+ label_roadmap: Planificació
+ label_roadmap_due_in: "Venç en {{value}}"
+ label_roadmap_overdue: "{{value}} tard"
+ label_roadmap_no_issues: No hi ha assumptes per a aquesta versió
+ label_search: Cerca
+ label_result_plural: Resultats
+ label_all_words: Totes les paraules
+ label_wiki: Wiki
+ label_wiki_edit: Edició wiki
+ label_wiki_edit_plural: Edicions wiki
+ label_wiki_page: Pàgina wiki
+ label_wiki_page_plural: Pàgines wiki
+ label_index_by_title: Índex per títol
+ label_index_by_date: Índex per data
+ label_current_version: Versió actual
+ label_preview: Previsualització
+ label_feed_plural: Canals
+ label_changes_details: Detalls de tots els canvis
+ label_issue_tracking: "Seguiment d'assumptes"
+ label_spent_time: Temps invertit
+ label_f_hour: "{{value}} hora"
+ label_f_hour_plural: "{{value}} hores"
+ label_time_tracking: Temps de seguiment
+ label_change_plural: Canvis
+ label_statistics: Estadístiques
+ label_commits_per_month: Publicacions per mes
+ label_commits_per_author: Publicacions per autor
+ label_view_diff: Visualitza les diferències
+ label_diff_inline: en línia
+ label_diff_side_by_side: costat per costat
+ label_options: Opcions
+ label_copy_workflow_from: Copia el flux de treball des de
+ label_permissions_report: Informe de permisos
+ label_watched_issues: Assumptes vigilats
+ label_related_issues: Assumptes relacionats
+ label_applied_status: Estat aplicat
+ label_loading: "S'està carregant..."
+ label_relation_new: Relació nova
+ label_relation_delete: Suprimeix la relació
+ label_relates_to: relacionat amb
+ label_duplicates: duplicats
+ label_duplicated_by: duplicat per
+ label_blocks: bloqueja
+ label_blocked_by: bloquejats per
+ label_precedes: anterior a
+ label_follows: posterior a
+ label_end_to_start: final al començament
+ label_end_to_end: final al final
+ label_start_to_start: començament al començament
+ label_start_to_end: començament al final
+ label_stay_logged_in: "Manté l'entrada"
+ label_disabled: inhabilitat
+ label_show_completed_versions: Mostra les versions completes
+ label_me: jo mateix
+ label_board: Fòrum
+ label_board_new: Fòrum nou
+ label_board_plural: Fòrums
+ label_topic_plural: Temes
+ label_message_plural: Missatges
+ label_message_last: Últim missatge
+ label_message_new: Missatge nou
+ label_message_posted: Missatge afegit
+ label_reply_plural: Respostes
+ label_send_information: "Envia la informació del compte a l'usuari"
+ label_year: Any
+ label_month: Mes
+ label_week: Setmana
+ label_date_from: Des de
+ label_date_to: A
+ label_language_based: "Basat en l'idioma de l'usuari"
+ label_sort_by: "Ordena per {{value}}"
+ label_send_test_email: Envia un correu electrònic de prova
+ label_feeds_access_key_created_on: "Clau d'accés del RSS creada fa {{value}}"
+ label_module_plural: Mòduls
+ label_added_time_by: "Afegit per {{author}} fa {{age}}"
+ label_updated_time_by: "Actualitzat per {{author}} fa {{age}}"
+ label_updated_time: "Actualitzat fa {{value}}"
+ label_jump_to_a_project: Salta al projecte...
+ label_file_plural: Fitxers
+ label_changeset_plural: Conjunt de canvis
+ label_default_columns: Columnes predeterminades
+ label_no_change_option: (sense canvis)
+ label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats
+ label_theme: Tema
+ label_default: Predeterminat
+ label_search_titles_only: Cerca només en els títols
+ label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes"
+ label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..."
+ label_user_mail_option_none: "Només per les coses que vigilo o hi estic implicat"
+ label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix"
+ label_registration_activation_by_email: activació del compte per correu electrònic
+ label_registration_manual_activation: activació del compte manual
+ label_registration_automatic_activation: activació del compte automàtica
+ label_display_per_page: "Per pàgina: {{value}}"
+ label_age: Edat
+ label_change_properties: Canvia les propietats
+ label_general: General
+ label_more: Més
+ label_scm: SCM
+ label_plugins: Connectors
+ label_ldap_authentication: Autenticació LDAP
+ label_downloads_abbr: Baixades
+ label_optional_description: Descripció opcional
+ label_add_another_file: Afegeix un altre fitxer
+ label_preferences: Preferències
+ label_chronological_order: En ordre cronològic
+ label_reverse_chronological_order: En ordre cronològic invers
+ label_planning: Planificació
+ label_incoming_emails: "Correu electrònics d'entrada"
+ label_generate_key: Genera una clau
+ label_issue_watchers: Vigilàncies
+ label_example: Exemple
+ label_display: Mostra
+ label_sort: Ordena
+ label_ascending: Ascendent
+ label_descending: Descendent
+ label_date_from_to: Des de {{start}} a {{end}}
+
+ button_login: Entra
+ button_submit: Tramet
+ button_save: Desa
+ button_check_all: Activa-ho tot
+ button_uncheck_all: Desactiva-ho tot
+ button_delete: Suprimeix
+ button_create: Crea
+ button_create_and_continue: Crea i continua
+ button_test: Test
+ button_edit: Edit
+ button_add: Afegeix
+ button_change: Canvia
+ button_apply: Aplica
+ button_clear: Neteja
+ button_lock: Bloca
+ button_unlock: Desbloca
+ button_download: Baixa
+ button_list: Llista
+ button_view: Visualitza
+ button_move: Mou
+ button_back: Enrere
+ button_cancel: Cancel·la
+ button_activate: Activa
+ button_sort: Ordena
+ button_log_time: "Hora d'entrada"
+ button_rollback: Torna a aquesta versió
+ button_watch: Vigila
+ button_unwatch: No vigilis
+ button_reply: Resposta
+ button_archive: Arxiva
+ button_unarchive: Desarxiva
+ button_reset: Reinicia
+ button_rename: Reanomena
+ button_change_password: Canvia la contrasenya
+ button_copy: Copia
+ button_annotate: Anota
+ button_update: Actualitza
+ button_configure: Configura
+ button_quote: Cita
+
+ status_active: actiu
+ status_registered: informat
+ status_locked: bloquejat
+
+ text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic."
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 significa sense restricció
+ text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades?
+ text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: {{value}}."
+ text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball
+ text_are_you_sure: Segur?
+ text_tip_task_begin_day: "tasca que s'inicia aquest dia"
+ text_tip_task_end_day: tasca que finalitza aquest dia
+ text_tip_task_begin_end_day: "tasca que s'inicia i finalitza aquest dia"
+ text_project_identifier_info: "Es permeten lletres en minúscules (a-z), números i guions.<br />Un cop desat, l'identificador no es pot modificar."
+ text_caracters_maximum: "{{count}} caràcters com a màxim."
+ text_caracters_minimum: "Com a mínim ha de tenir {{count}} caràcters."
+ text_length_between: "Longitud entre {{min}} i {{max}} caràcters."
+ text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor"
+ text_unallowed_characters: Caràcters no permesos
+ text_comma_separated: Es permeten valors múltiples (separats per una coma).
+ text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats
+ text_issue_added: "L'assumpte {{id}} ha sigut informat per {{author}}."
+ text_issue_updated: "L'assumpte {{id}} ha sigut actualitzat per {{author}}."
+ text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts?
+ text_issue_category_destroy_question: "Alguns assumptes ({{count}}) estan assignats a aquesta categoria. Què voleu fer?"
+ text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria
+ text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria
+ text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)."
+ text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada."
+ text_load_default_configuration: Carrega la configuració predeterminada
+ text_status_changed_by_changeset: "Aplicat en el conjunt de canvis {{value}}."
+ text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?"
+ text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:"
+ text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat"
+ text_file_repository_writable: Es pot escriure en el dipòsit de fitxers
+ text_plugin_assets_writable: Es pot escriure als connectors actius
+ text_rmagick_available: RMagick disponible (opcional)
+ text_destroy_time_entries_question: "S'han informat {{hours}} hores en els assumptes que aneu a suprimir. Què voleu fer?"
+ text_destroy_time_entries: Suprimeix les hores informades
+ text_assign_time_entries_to_project: Assigna les hores informades al projecte
+ text_reassign_time_entries: 'Torna a assignar les hores informades a aquest assumpte:'
+ text_user_wrote: "{{value}} va escriure:"
+ text_enumeration_destroy_question: "{{count}} objectes estan assignats a aquest valor."
+ text_enumeration_category_reassign_to: 'Torna a assignar-los a aquest valor:'
+ text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/email.yml i reinicieu l'aplicació per habilitar-lo."
+ text_repository_usernames_mapping: "Seleccioneu l'assignació entre els usuaris del Redmine i cada nom d'usuari trobat al dipòsit.\nEls usuaris amb el mateix nom d'usuari o correu del Redmine i del dipòsit s'assignaran automàticament."
+ text_diff_truncated: "... Aquestes diferències s'han trucat perquè excedeixen la mida màxima que es pot mostrar."
+ text_custom_field_possible_values_info: 'Una línia per a cada valor'
+
+ default_role_manager: Gestor
+ default_role_developper: Desenvolupador
+ default_role_reporter: Informador
+ default_tracker_bug: Error
+ default_tracker_feature: Característica
+ default_tracker_support: Suport
+ default_issue_status_new: Nou
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Resolt
+ default_issue_status_feedback: Comentaris
+ default_issue_status_closed: Tancat
+ default_issue_status_rejected: Rebutjat
+ default_doc_category_user: "Documentació d'usuari"
+ default_doc_category_tech: Documentació tècnica
+ default_priority_low: Baixa
+ default_priority_normal: Normal
+ default_priority_high: Alta
+ default_priority_urgent: Urgent
+ default_priority_immediate: Immediata
+ default_activity_design: Disseny
+ default_activity_development: Desenvolupament
+
+ enumeration_issue_priorities: Prioritat dels assumptes
+ enumeration_doc_categories: Categories del document
+ enumeration_activities: Activitats (seguidor de temps)
+ 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: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+cs:
+ 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: [Neděle, Pondělí, Úterý, Středa, Čtvrtek, Pátek, Sobota]
+ abbr_day_names: [Ne, Po, Út, St, Čt, Pá, So]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Leden, Únor, Březen, Duben, Květen, Červen, Červenec, Srpen, Září, Říjen, Listopad, Prosinec]
+ abbr_month_names: [~, Led, Úno, Bře, Dub, Kvě, Čer, Čec, Srp, Zář, Říj, Lis, Pro]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "půl minuty"
+ less_than_x_seconds:
+ one: "méně než sekunda"
+ other: "méně než {{count}} sekund"
+ x_seconds:
+ one: "1 sekunda"
+ other: "{{count}} sekund"
+ less_than_x_minutes:
+ one: "méně než minuta"
+ other: "méně než {{count}} minut"
+ x_minutes:
+ one: "1 minuta"
+ other: "{{count}} minut"
+ about_x_hours:
+ one: "asi 1 hodina"
+ other: "asi {{count}} hodin"
+ x_days:
+ one: "1 den"
+ other: "{{count}} dnů"
+ about_x_months:
+ one: "asi 1 měsíc"
+ other: "asi {{count}} měsíců"
+ x_months:
+ one: "1 měsíc"
+ other: "{{count}} měsíců"
+ about_x_years:
+ one: "asi 1 rok"
+ other: "asi {{count}} let"
+ over_x_years:
+ one: "více než 1 rok"
+ other: "více než {{count}} roky"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "a"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "není zahrnuto v seznamu"
+ exclusion: "je rezervováno"
+ invalid: "je neplatné"
+ confirmation: "se neshoduje s potvrzením"
+ accepted: "musí být akceptováno"
+ empty: "nemůže být prázdný"
+ blank: "nemůže být prázdný"
+ too_long: "je příliš dlouhý"
+ too_short: "je příliš krátký"
+ wrong_length: "má chybnou délku"
+ taken: "je již použito"
+ not_a_number: "není číslo"
+ not_a_date: "není platné datum"
+ greater_than: "musí být větší než {{count}}"
+ greater_than_or_equal_to: "musí být větší nebo rovno {{count}}"
+ equal_to: "musí být přesně {{count}}"
+ less_than: "musí být méně než {{count}}"
+ less_than_or_equal_to: "musí být méně nebo rovno {{count}}"
+ odd: "musí být liché"
+ even: "musí být sudé"
+ greater_than_start_date: "musí být větší než počáteční datum"
+ not_same_project: "nepatří stejnému projektu"
+ circular_dependency: "Tento vztah by vytvořil cyklickou závislost"
+
+ # Updated by Josef Liška <jl@chl.cz>
+ # CZ translation by Maxim Krušina | Massimo Filippi, s.r.o. | maxim@mxm.cz
+ # Based on original CZ translation by Jan Kadleček
+
+ actionview_instancetag_blank_option: Prosím vyberte
+
+ general_text_No: 'Ne'
+ general_text_Yes: 'Ano'
+ general_text_no: 'ne'
+ general_text_yes: 'ano'
+ general_lang_name: 'Čeština'
+ 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: Účet byl úspěšně změněn.
+ notice_account_invalid_creditentials: Chybné jméno nebo heslo
+ notice_account_password_updated: Heslo bylo úspěšně změněno.
+ notice_account_wrong_password: Chybné heslo
+ notice_account_register_done: Účet byl úspěšně vytvořen. Pro aktivaci účtu klikněte na odkaz v emailu, který vám byl zaslán.
+ notice_account_unknown_email: Neznámý uživatel.
+ notice_can_t_change_password: Tento účet používá externí autentifikaci. Zde heslo změnit nemůžete.
+ notice_account_lost_email_sent: Byl vám zaslán email s intrukcemi jak si nastavíte nové heslo.
+ notice_account_activated: Váš účet byl aktivován. Nyní se můžete přihlásit.
+ notice_successful_create: Úspěšně vytvořeno.
+ notice_successful_update: Úspěšně aktualizováno.
+ notice_successful_delete: Úspěšně odstraněno.
+ notice_successful_connection: Úspěšné připojení.
+ notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána.
+ notice_locking_conflict: Údaje byly změněny jiným uživatelem.
+ notice_scm_error: Entry and/or revision doesn't exist in the repository.
+ notice_not_authorized: Nemáte dostatečná práva pro zobrazení této stránky.
+ notice_email_sent: "Na adresu {{value}} byl odeslán email"
+ notice_email_error: "Při odesílání emailu nastala chyba ({{value}})"
+ notice_feeds_access_key_reseted: Váš klíč pro přístup k RSS byl resetován.
+ notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
+ notice_no_issue_selected: "Nebyl zvolen žádný úkol. Prosím, zvolte úkoly, které chcete editovat"
+ notice_account_pending: "Váš účet byl vytvořen, nyní čeká na schválení administrátorem."
+ notice_default_data_loaded: Výchozí konfigurace úspěšně nahrána.
+
+ error_can_t_load_default_data: "Výchozí konfigurace nebyla nahrána: {{value}}"
+ error_scm_not_found: "Položka a/nebo revize neexistují v repository."
+ error_scm_command_failed: "Při pokusu o přístup k repository došlo k chybě: {{value}}"
+ error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu'
+
+ mail_subject_lost_password: "Vaše heslo ({{value}})"
+ mail_body_lost_password: 'Pro změnu vašeho hesla klikněte na následující odkaz:'
+ mail_subject_register: "Aktivace účtu ({{value}})"
+ mail_body_register: 'Pro aktivaci vašeho účtu klikněte na následující odkaz:'
+ mail_body_account_information_external: "Pomocí vašeho účtu {{value}} se můžete přihlásit."
+ mail_body_account_information: Informace o vašem účtu
+ mail_subject_account_activation_request: "Aktivace {{value}} účtu"
+ mail_body_account_activation_request: "Byl zaregistrován nový uživatel {{value}}. Aktivace jeho účtu závisí na vašem potvrzení."
+
+ gui_validation_error: 1 chyba
+ gui_validation_error_plural: "{{count}} chyb(y)"
+
+ field_name: Název
+ field_description: Popis
+ field_summary: Přehled
+ field_is_required: Povinné pole
+ field_firstname: Jméno
+ field_lastname: Příjmení
+ field_mail: Email
+ field_filename: Soubor
+ field_filesize: Velikost
+ field_downloads: Staženo
+ field_author: Autor
+ field_created_on: Vytvořeno
+ field_updated_on: Aktualizováno
+ field_field_format: Formát
+ field_is_for_all: Pro všechny projekty
+ field_possible_values: Možné hodnoty
+ field_regexp: Regulární výraz
+ field_min_length: Minimální délka
+ field_max_length: Maximální délka
+ field_value: Hodnota
+ field_category: Kategorie
+ field_title: Název
+ field_project: Projekt
+ field_issue: Úkol
+ field_status: Stav
+ field_notes: Poznámka
+ field_is_closed: Úkol uzavřen
+ field_is_default: Výchozí stav
+ field_tracker: Fronta
+ field_subject: Předmět
+ field_due_date: Uzavřít do
+ field_assigned_to: Přiřazeno
+ field_priority: Priorita
+ field_fixed_version: Přiřazeno k verzi
+ field_user: Uživatel
+ field_role: Role
+ field_homepage: Homepage
+ field_is_public: Veřejný
+ field_parent: Nadřazený projekt
+ field_is_in_chlog: Úkoly zobrazené v změnovém logu
+ field_is_in_roadmap: Úkoly zobrazené v plánu
+ field_login: Přihlášení
+ field_mail_notification: Emailová oznámení
+ field_admin: Administrátor
+ field_last_login_on: Poslední přihlášení
+ field_language: Jazyk
+ field_effective_date: Datum
+ field_password: Heslo
+ field_new_password: Nové heslo
+ field_password_confirmation: Potvrzení
+ field_version: Verze
+ field_type: Typ
+ field_host: Host
+ field_port: Port
+ field_account: Účet
+ field_base_dn: Base DN
+ field_attr_login: Přihlášení (atribut)
+ field_attr_firstname: Jméno (atribut)
+ field_attr_lastname: Příjemní (atribut)
+ field_attr_mail: Email (atribut)
+ field_onthefly: Automatické vytváření uživatelů
+ field_start_date: Začátek
+ field_done_ratio: % Hotovo
+ field_auth_source: Autentifikační mód
+ field_hide_mail: Nezobrazovat můj email
+ field_comments: Komentář
+ field_url: URL
+ field_start_page: Výchozí stránka
+ field_subproject: Podprojekt
+ field_hours: Hodiny
+ field_activity: Aktivita
+ field_spent_on: Datum
+ field_identifier: Identifikátor
+ field_is_filter: Použít jako filtr
+ field_issue_to: Související úkol
+ field_delay: Zpoždění
+ field_assignable: Úkoly mohou být přiřazeny této roli
+ field_redirect_existing_links: Přesměrovat stvávající odkazy
+ field_estimated_hours: Odhadovaná doba
+ field_column_names: Sloupce
+ field_time_zone: Časové pásmo
+ field_searchable: Umožnit vyhledávání
+ field_default_value: Výchozí hodnota
+ field_comments_sorting: Zobrazit komentáře
+
+ setting_app_title: Název aplikace
+ setting_app_subtitle: Podtitulek aplikace
+ setting_welcome_text: Uvítací text
+ setting_default_language: Výchozí jazyk
+ setting_login_required: Auten. vyžadována
+ setting_self_registration: Povolena automatická registrace
+ setting_attachment_max_size: Maximální velikost přílohy
+ setting_issues_export_limit: Limit pro export úkolů
+ setting_mail_from: Odesílat emaily z adresy
+ setting_bcc_recipients: Příjemci skryté kopie (bcc)
+ setting_host_name: Host name
+ setting_text_formatting: Formátování textu
+ setting_wiki_compression: Komperese historie Wiki
+ setting_feeds_limit: Feed content limit
+ setting_default_projects_public: Nové projekty nastavovat jako veřejné
+ setting_autofetch_changesets: Autofetch commits
+ setting_sys_api_enabled: Povolit WS pro správu repozitory
+ setting_commit_ref_keywords: Klíčová slova pro odkazy
+ setting_commit_fix_keywords: Klíčová slova pro uzavření
+ setting_autologin: Automatické přihlašování
+ setting_date_format: Formát data
+ setting_time_format: Formát času
+ setting_cross_project_issue_relations: Povolit vazby úkolů napříč projekty
+ setting_issue_list_default_columns: Výchozí sloupce zobrazené v seznamu úkolů
+ setting_repositories_encodings: Kódování
+ setting_emails_footer: Patička emailů
+ setting_protocol: Protokol
+ setting_per_page_options: Povolené počty řádků na stránce
+ setting_user_format: Formát zobrazení uživatele
+ setting_activity_days_default: Days displayed on project activity
+ setting_display_subprojects_issues: Display subprojects issues on main projects by default
+
+ project_module_issue_tracking: Sledování úkolů
+ project_module_time_tracking: Sledování času
+ project_module_news: Novinky
+ project_module_documents: Dokumenty
+ project_module_files: Soubory
+ project_module_wiki: Wiki
+ project_module_repository: Repository
+ project_module_boards: Diskuse
+
+ label_user: Uživatel
+ label_user_plural: Uživatelé
+ label_user_new: Nový uživatel
+ label_project: Projekt
+ label_project_new: Nový projekt
+ label_project_plural: Projekty
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Všechny projekty
+ label_project_latest: Poslední projekty
+ label_issue: Úkol
+ label_issue_new: Nový úkol
+ label_issue_plural: Úkoly
+ label_issue_view_all: Všechny úkoly
+ label_issues_by: "Úkoly od uživatele {{value}}"
+ label_issue_added: Úkol přidán
+ label_issue_updated: Úkol aktualizován
+ label_document: Dokument
+ label_document_new: Nový dokument
+ label_document_plural: Dokumenty
+ label_document_added: Dokument přidán
+ label_role: Role
+ label_role_plural: Role
+ label_role_new: Nová role
+ label_role_and_permissions: Role a práva
+ label_member: Člen
+ label_member_new: Nový člen
+ label_member_plural: Členové
+ label_tracker: Fronta
+ label_tracker_plural: Fronty
+ label_tracker_new: Nová fronta
+ label_workflow: Workflow
+ label_issue_status: Stav úkolu
+ label_issue_status_plural: Stavy úkolů
+ label_issue_status_new: Nový stav
+ label_issue_category: Kategorie úkolu
+ label_issue_category_plural: Kategorie úkolů
+ label_issue_category_new: Nová kategorie
+ label_custom_field: Uživatelské pole
+ label_custom_field_plural: Uživatelská pole
+ label_custom_field_new: Nové uživatelské pole
+ label_enumerations: Seznamy
+ label_enumeration_new: Nová hodnota
+ label_information: Informace
+ label_information_plural: Informace
+ label_please_login: Prosím přihlašte se
+ label_register: Registrovat
+ label_password_lost: Zapomenuté heslo
+ label_home: Úvodní
+ label_my_page: Moje stránka
+ label_my_account: Můj účet
+ label_my_projects: Moje projekty
+ label_administration: Administrace
+ label_login: Přihlášení
+ label_logout: Odhlášení
+ label_help: Nápověda
+ label_reported_issues: Nahlášené úkoly
+ label_assigned_to_me_issues: Mé úkoly
+ label_last_login: Poslední přihlášení
+ label_registered_on: Registrován
+ label_activity: Aktivita
+ label_overall_activity: Celková aktivita
+ label_new: Nový
+ label_logged_as: Přihlášen jako
+ label_environment: Prostředí
+ label_authentication: Autentifikace
+ label_auth_source: Mód autentifikace
+ label_auth_source_new: Nový mód autentifikace
+ label_auth_source_plural: Módy autentifikace
+ label_subproject_plural: Podprojekty
+ label_min_max_length: Min - Max délka
+ label_list: Seznam
+ label_date: Datum
+ label_integer: Celé číslo
+ label_float: Desetiné číslo
+ label_boolean: Ano/Ne
+ label_string: Text
+ label_text: Dlouhý text
+ label_attribute: Atribut
+ label_attribute_plural: Atributy
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Žádné položky
+ label_change_status: Změnit stav
+ label_history: Historie
+ label_attachment: Soubor
+ label_attachment_new: Nový soubor
+ label_attachment_delete: Odstranit soubor
+ label_attachment_plural: Soubory
+ label_file_added: Soubor přidán
+ label_report: Přeheled
+ label_report_plural: Přehledy
+ label_news: Novinky
+ label_news_new: Přidat novinku
+ label_news_plural: Novinky
+ label_news_latest: Poslední novinky
+ label_news_view_all: Zobrazit všechny novinky
+ label_news_added: Novinka přidána
+ label_change_log: Protokol změn
+ label_settings: Nastavení
+ label_overview: Přehled
+ label_version: Verze
+ label_version_new: Nová verze
+ label_version_plural: Verze
+ label_confirmation: Potvrzení
+ label_export_to: 'Také k dispozici:'
+ label_read: Načítá se...
+ label_public_projects: Veřejné projekty
+ label_open_issues: otevřený
+ label_open_issues_plural: otevřené
+ label_closed_issues: uzavřený
+ label_closed_issues_plural: uzavřené
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Celkem
+ label_permissions: Práva
+ label_current_status: Aktuální stav
+ label_new_statuses_allowed: Nové povolené stavy
+ label_all: vše
+ label_none: nic
+ label_nobody: nikdo
+ label_next: Další
+ label_previous: Předchozí
+ label_used_by: Použito
+ label_details: Detaily
+ label_add_note: Přidat poznámku
+ label_per_page: Na stránku
+ label_calendar: Kalendář
+ label_months_from: měsíců od
+ label_gantt: Ganttův graf
+ label_internal: Interní
+ label_last_changes: "posledních {{count}} změn"
+ label_change_view_all: Zobrazit všechny změny
+ label_personalize_page: Přizpůsobit tuto stránku
+ label_comment: Komentář
+ label_comment_plural: Komentáře
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Přidat komentáře
+ label_comment_added: Komentář přidán
+ label_comment_delete: Odstranit komentář
+ label_query: Uživatelský dotaz
+ label_query_plural: Uživatelské dotazy
+ label_query_new: Nový dotaz
+ label_filter_add: Přidat filtr
+ label_filter_plural: Filtry
+ label_equals: je
+ label_not_equals: není
+ label_in_less_than: je měší než
+ label_in_more_than: je větší než
+ label_in: v
+ label_today: dnes
+ label_all_time: vše
+ label_yesterday: včera
+ label_this_week: tento týden
+ label_last_week: minulý týden
+ label_last_n_days: "posledních {{count}} dnů"
+ label_this_month: tento měsíc
+ label_last_month: minulý měsíc
+ label_this_year: tento rok
+ label_date_range: Časový rozsah
+ label_less_than_ago: před méně jak (dny)
+ label_more_than_ago: před více jak (dny)
+ label_ago: před (dny)
+ label_contains: obsahuje
+ label_not_contains: neobsahuje
+ label_day_plural: dny
+ label_repository: Repository
+ label_repository_plural: Repository
+ label_browse: Procházet
+ label_modification: "{{count}} změna"
+ label_modification_plural: "{{count}} změn"
+ label_revision: Revize
+ label_revision_plural: Revizí
+ label_associated_revisions: Související verze
+ label_added: přidáno
+ label_modified: změněno
+ label_deleted: odstraněno
+ label_latest_revision: Poslední revize
+ label_latest_revision_plural: Poslední revize
+ label_view_revisions: Zobrazit revize
+ label_max_size: Maximální velikost
+ label_sort_highest: Přesunout na začátek
+ label_sort_higher: Přesunout nahoru
+ label_sort_lower: Přesunout dolů
+ label_sort_lowest: Přesunout na konec
+ label_roadmap: Plán
+ label_roadmap_due_in: "Zbývá {{value}}"
+ label_roadmap_overdue: "{{value}} pozdě"
+ label_roadmap_no_issues: Pro tuto verzi nejsou žádné úkoly
+ label_search: Hledat
+ label_result_plural: Výsledky
+ label_all_words: Všechna slova
+ label_wiki: Wiki
+ label_wiki_edit: Wiki úprava
+ label_wiki_edit_plural: Wiki úpravy
+ label_wiki_page: Wiki stránka
+ label_wiki_page_plural: Wiki stránky
+ label_index_by_title: Index dle názvu
+ label_index_by_date: Index dle data
+ label_current_version: Aktuální verze
+ label_preview: Náhled
+ label_feed_plural: Příspěvky
+ label_changes_details: Detail všech změn
+ label_issue_tracking: Sledování úkolů
+ label_spent_time: Strávený čas
+ label_f_hour: "{{value}} hodina"
+ label_f_hour_plural: "{{value}} hodin"
+ label_time_tracking: Sledování času
+ label_change_plural: Změny
+ label_statistics: Statistiky
+ label_commits_per_month: Commitů za měsíc
+ label_commits_per_author: Commitů za autora
+ label_view_diff: Zobrazit rozdíly
+ label_diff_inline: uvnitř
+ label_diff_side_by_side: vedle sebe
+ label_options: Nastavení
+ label_copy_workflow_from: Kopírovat workflow z
+ label_permissions_report: Přehled práv
+ label_watched_issues: Sledované úkoly
+ label_related_issues: Související úkoly
+ label_applied_status: Použitý stav
+ label_loading: Nahrávám...
+ label_relation_new: Nová souvislost
+ label_relation_delete: Odstranit souvislost
+ label_relates_to: související s
+ label_duplicates: duplicity
+ label_blocks: bloků
+ label_blocked_by: zablokován
+ label_precedes: předchází
+ label_follows: následuje
+ label_end_to_start: od konce do začátku
+ label_end_to_end: od konce do konce
+ label_start_to_start: od začátku do začátku
+ label_start_to_end: od začátku do konce
+ label_stay_logged_in: Zůstat přihlášený
+ label_disabled: zakázán
+ label_show_completed_versions: Ukázat dokončené verze
+ label_me: mě
+ label_board: Fórum
+ label_board_new: Nové fórum
+ label_board_plural: Fóra
+ label_topic_plural: Témata
+ label_message_plural: Zprávy
+ label_message_last: Poslední zpráva
+ label_message_new: Nová zpráva
+ label_message_posted: Zpráva přidána
+ label_reply_plural: Odpovědi
+ label_send_information: Zaslat informace o účtu uživateli
+ label_year: Rok
+ label_month: Měsíc
+ label_week: Týden
+ label_date_from: Od
+ label_date_to: Do
+ label_language_based: Podle výchozího jazyku
+ label_sort_by: "Seřadit podle {{value}}"
+ label_send_test_email: Poslat testovací email
+ label_feeds_access_key_created_on: "Přístupový klíč pro RSS byl vytvořen před {{value}}"
+ label_module_plural: Moduly
+ label_added_time_by: "Přidáno uživatelem {{author}} před {{age}}"
+ label_updated_time: "Aktualizováno před {{value}}"
+ label_jump_to_a_project: Zvolit projekt...
+ label_file_plural: Soubory
+ label_changeset_plural: Changesety
+ label_default_columns: Výchozí sloupce
+ label_no_change_option: (beze změny)
+ label_bulk_edit_selected_issues: Bulk edit selected issues
+ label_theme: Téma
+ label_default: Výchozí
+ label_search_titles_only: Vyhledávat pouze v názvech
+ label_user_mail_option_all: "Pro všechny události všech mých projektů"
+ label_user_mail_option_selected: "Pro všechny události vybraných projektů..."
+ label_user_mail_option_none: "Pouze pro události které sleduji nebo které se mne týkají"
+ label_user_mail_no_self_notified: "Nezasílat informace o mnou vytvořených změnách"
+ label_registration_activation_by_email: aktivace účtu emailem
+ label_registration_manual_activation: manuální aktivace účtu
+ label_registration_automatic_activation: automatická aktivace účtu
+ label_display_per_page: "{{value}} na stránku"
+ label_age: Věk
+ label_change_properties: Změnit vlastnosti
+ label_general: Obecné
+ label_more: Více
+ label_scm: SCM
+ label_plugins: Doplňky
+ label_ldap_authentication: Autentifikace LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Volitelný popis
+ label_add_another_file: Přidat další soubor
+ label_preferences: Nastavení
+ label_chronological_order: V chronologickém pořadí
+ label_reverse_chronological_order: V obrácaném chronologickém pořadí
+
+ button_login: Přihlásit
+ button_submit: Potvrdit
+ button_save: Uložit
+ button_check_all: Zašrtnout vše
+ button_uncheck_all: Odšrtnout vše
+ button_delete: Odstranit
+ button_create: Vytvořit
+ button_test: Test
+ button_edit: Upravit
+ button_add: Přidat
+ button_change: Změnit
+ button_apply: Použít
+ button_clear: Smazat
+ button_lock: Zamknout
+ button_unlock: Odemknout
+ button_download: Stáhnout
+ button_list: Vypsat
+ button_view: Zobrazit
+ button_move: Přesunout
+ button_back: Zpět
+ button_cancel: Storno
+ button_activate: Aktivovat
+ button_sort: Seřadit
+ button_log_time: Přidat čas
+ button_rollback: Zpět k této verzi
+ button_watch: Sledovat
+ button_unwatch: Nesledovat
+ button_reply: Odpovědět
+ button_archive: Archivovat
+ button_unarchive: Odarchivovat
+ button_reset: Reset
+ button_rename: Přejmenovat
+ button_change_password: Změnit heslo
+ button_copy: Kopírovat
+ button_annotate: Komentovat
+ button_update: Aktualizovat
+ button_configure: Konfigurovat
+
+ status_active: aktivní
+ status_registered: registrovaný
+ status_locked: uzamčený
+
+ text_select_mail_notifications: Vyberte akci při které bude zasláno upozornění emailem.
+ text_regexp_info: např. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 znamená bez limitu
+ text_project_destroy_confirmation: Jste si jisti, že chcete odstranit tento projekt a všechna související data ?
+ text_workflow_edit: Vyberte roli a frontu k editaci workflow
+ text_are_you_sure: Jste si jisti?
+ text_tip_task_begin_day: úkol začíná v tento den
+ text_tip_task_end_day: úkol končí v tento den
+ text_tip_task_begin_end_day: úkol začíná a končí v tento den
+ text_project_identifier_info: 'Jsou povolena malá písmena (a-z), čísla a pomlčky.<br />Po uložení již není možné identifikátor změnit.'
+ text_caracters_maximum: "{{count}} znaků maximálně."
+ text_caracters_minimum: "Musí být alespoň {{count}} znaků dlouhé."
+ text_length_between: "Délka mezi {{min}} a {{max}} znaky."
+ text_tracker_no_workflow: Pro tuto frontu není definován žádný workflow
+ text_unallowed_characters: Nepovolené znaky
+ text_comma_separated: Povoleno více hodnot (oddělěné čárkou).
+ text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+ text_issue_added: "Úkol {{id}} byl vytvořen uživatelem {{author}}."
+ text_issue_updated: "Úkol {{id}} byl aktualizován uživatelem {{author}}."
+ text_wiki_destroy_confirmation: Opravdu si přejete odstranit tuto WIKI a celý její obsah?
+ text_issue_category_destroy_question: "Některé úkoly ({{count}}) jsou přiřazeny k této kategorii. Co s nimi chtete udělat?"
+ text_issue_category_destroy_assignments: Zrušit přiřazení ke kategorii
+ text_issue_category_reassign_to: Přiřadit úkoly do této kategorie
+ text_user_mail_option: "U projektů, které nebyly vybrány, budete dostávat oznámení pouze o vašich či o sledovaných položkách (např. o položkách jejichž jste autor nebo ke kterým jste přiřazen(a))."
+ text_no_configuration_data: "Role, fronty, stavy úkolů ani workflow nebyly zatím nakonfigurovány.\nVelice doporučujeme nahrát výchozí konfiguraci.Po té si můžete vše upravit"
+ text_load_default_configuration: Nahrát výchozí konfiguraci
+ text_status_changed_by_changeset: "Použito v changesetu {{value}}."
+ text_issues_destroy_confirmation: 'Opravdu si přejete odstranit všechny zvolené úkoly?'
+ text_select_project_modules: 'Aktivní moduly v tomto projektu:'
+ text_default_administrator_account_changed: Výchozí nastavení administrátorského účtu změněno
+ text_file_repository_writable: Povolen zápis do repository
+ text_rmagick_available: RMagick k dispozici (volitelné)
+ text_destroy_time_entries_question: "U úkolů, které chcete odstranit je evidováno {{hours}} práce. Co chete udělat?"
+ text_destroy_time_entries: Odstranit evidované hodiny.
+ text_assign_time_entries_to_project: Přiřadit evidované hodiny projektu
+ text_reassign_time_entries: 'Přeřadit evidované hodiny k tomuto úkolu:'
+
+ default_role_manager: Manažer
+ default_role_developper: Vývojář
+ default_role_reporter: Reportér
+ default_tracker_bug: Chyba
+ default_tracker_feature: Požadavek
+ default_tracker_support: Podpora
+ default_issue_status_new: Nový
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Vyřešený
+ default_issue_status_feedback: Čeká se
+ default_issue_status_closed: Uzavřený
+ default_issue_status_rejected: Odmítnutý
+ default_doc_category_user: Uživatelská dokumentace
+ default_doc_category_tech: Technická dokumentace
+ default_priority_low: Nízká
+ default_priority_normal: Normální
+ default_priority_high: Vysoká
+ default_priority_urgent: Urgentní
+ default_priority_immediate: Okamžitá
+ default_activity_design: Design
+ default_activity_development: Vývoj
+
+ enumeration_issue_priorities: Priority úkolů
+ enumeration_doc_categories: Kategorie dokumentů
+ enumeration_activities: Aktivity (sledování času)
+ error_scm_annotate: "Položka neexistuje nebo nemůže být komentována."
+ label_planning: Plánování
+ text_subprojects_destroy_warning: "Jeho podprojek(y): {{value}} budou také smazány."
+ label_and_its_subprojects: "{{value}} a jeho podprojekty"
+ mail_body_reminder: "{{count}} úkol(ů), které máte přiřazeny má termín během několik dní ({{days}}):"
+ mail_subject_reminder: "{{count}} úkol(ů) má termín během několik dní"
+ text_user_wrote: "{{value}} napsal:"
+ label_duplicated_by: duplicated by
+ setting_enabled_scm: Povoleno SCM
+ text_enumeration_category_reassign_to: 'Přeřadit je do této:'
+ text_enumeration_destroy_question: "Několik ({{count}}) objektů je přiřazeno k této hodnotě."
+ label_incoming_emails: Příchozí e-maily
+ label_generate_key: Generovat klíč
+ setting_mail_handler_api_enabled: Povolit WS pro příchozí e-maily
+ setting_mail_handler_api_key: API klíč
+ text_email_delivery_not_configured: "Doručování e-mailů není nastaveno a odesílání notifikací je zakázáno.\nNastavte Váš SMTP server v souboru config/email.yml a restartujte aplikaci."
+ field_parent_title: Rodičovská stránka
+ label_issue_watchers: Sledování
+ setting_commit_logs_encoding: Kódování zpráv při commitu
+ button_quote: Citovat
+ setting_sequential_project_identifiers: Generovat sekvenční identifikátory projektů
+ notice_unable_delete_version: Nemohu odstanit verzi
+ label_renamed: přejmenováno
+ label_copied: zkopírováno
+ setting_plain_text_mail: pouze prostý text (ne HTML)
+ permission_view_files: Prohlížení souborů
+ permission_edit_issues: Upravování úkolů
+ permission_edit_own_time_entries: Upravování vlastních zázamů o stráveném čase
+ permission_manage_public_queries: Správa veřejných dotazů
+ permission_add_issues: Přidávání úkolů
+ permission_log_time: Zaznamenávání stráveného času
+ permission_view_changesets: Zobrazování sady změn
+ permission_view_time_entries: Zobrazení stráveného času
+ permission_manage_versions: Spravování verzí
+ permission_manage_wiki: Spravování wiki
+ permission_manage_categories: Spravování kategorií úkolů
+ permission_protect_wiki_pages: Zabezpečení wiki stránek
+ permission_comment_news: Komentování novinek
+ permission_delete_messages: Mazání zpráv
+ permission_select_project_modules: Výběr modulů projektu
+ permission_manage_documents: Správa dokumentů
+ permission_edit_wiki_pages: Upravování stránek wiki
+ permission_add_issue_watchers: Přidání sledujících uživatelů
+ permission_view_gantt: Zobrazené Ganttova diagramu
+ permission_move_issues: Přesouvání úkolů
+ permission_manage_issue_relations: Spravování vztahů mezi úkoly
+ permission_delete_wiki_pages: Mazání stránek na wiki
+ permission_manage_boards: Správa diskusních fór
+ permission_delete_wiki_pages_attachments: Mazání příloh
+ permission_view_wiki_edits: Prohlížení historie wiki
+ permission_add_messages: Posílání zpráv
+ permission_view_messages: Prohlížení zpráv
+ permission_manage_files: Spravování souborů
+ permission_edit_issue_notes: Upravování poznámek
+ permission_manage_news: Spravování novinek
+ permission_view_calendar: Prohlížení kalendáře
+ permission_manage_members: Spravování členství
+ permission_edit_messages: Upravování zpráv
+ permission_delete_issues: Mazání úkolů
+ permission_view_issue_watchers: Zobrazení seznamu sledujícíh uživatelů
+ permission_manage_repository: Spravování repozitáře
+ permission_commit_access: Commit access
+ permission_browse_repository: Procházení repozitáře
+ permission_view_documents: Prohlížení dokumentů
+ permission_edit_project: Úprava projektů
+ permission_add_issue_notes: Přidávání poznámek
+ permission_save_queries: Ukládání dotazů
+ permission_view_wiki_pages: Prohlížení wiki
+ permission_rename_wiki_pages: Přejmenovávání wiki stránek
+ permission_edit_time_entries: Upravování záznamů o stráveném času
+ permission_edit_own_issue_notes: Upravování vlastních poznámek
+ setting_gravatar_enabled: Použít uživatelské ikony Gravatar
+ label_example: Příklad
+ text_repository_usernames_mapping: "Vybrat nebo upravit mapování mezi Redmine uživateli a uživatelskými jmény nalezenými v logu repozitáře.\nUživatelé se shodným Redmine uživateslkým jménem a uživatelským jménem v repozitáři jsou mapovaní automaticky."
+ permission_edit_own_messages: Upravit vlastní zprávy
+ permission_delete_own_messages: Smazat vlastní zprávy
+ label_user_activity: "Aktivita uživatele: {{value}}"
+ label_updated_time_by: "Akutualizováno: {{author}} před: {{age}}"
+ text_diff_truncated: '... Rozdílový soubor je zkrácen, protože jeho délka přesahuje max. limit.'
+ setting_diff_max_lines_displayed: Maximální počet zobrazenách řádků rozdílů
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} soubor(ů) nebylo možné uložit."
+ button_create_and_continue: Vytvořit a pokračovat
+ text_custom_field_possible_values_info: 'Každá hodnota na novém řádku'
+ label_display: Zobrazit
+ field_editable: Editovatelný
+ setting_repository_log_display_limit: Maximální počet revizí zobrazených v logu souboru
+ setting_file_max_size_displayed: Maximální velikost textových souborů zobrazených přímo na stránce
+ field_watcher: Sleduje
+ setting_openid: Umožnit přihlašování a registrace s OpenID
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: nebo se přihlašte s OpenID
+ field_content: Obsah
+ label_descending: Sestupně
+ label_sort: Řazení
+ label_ascending: Vzestupně
+ label_date_from_to: Od {{start}} do {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Danish translation file for standard Ruby on Rails internationalization
+# by Lars Hoeg (http://www.lenio.dk/)
+# updated and upgraded to 0.9 by Morten Krogh Andersen (http://www.krogh.net)
+
+da:
+ date:
+ formats:
+ default: "%d.%m.%Y"
+ short: "%e. %b %Y"
+ long: "%e. %B %Y"
+
+ day_names: [søndag, mandag, tirsdag, onsdag, torsdag, fredag, lørdag]
+ abbr_day_names: [sø, ma, ti, 'on', to, fr, lø]
+ month_names: [~, januar, februar, marts, april, maj, juni, juli, august, september, oktober, november, december]
+ abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, aug, sep, okt, nov, dec]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%e. %B %Y, %H:%M"
+ time: "%H:%M"
+ short: "%e. %b %Y, %H:%M"
+ long: "%A, %e. %B %Y, %H:%M"
+ am: ""
+ pm: ""
+
+ support:
+ array:
+ sentence_connector: "og"
+ skip_last_comma: true
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "et halvt minut"
+ less_than_x_seconds:
+ one: "mindre end et sekund"
+ other: "mindre end {{count}} sekunder"
+ x_seconds:
+ one: "et sekund"
+ other: "{{count}} sekunder"
+ less_than_x_minutes:
+ one: "mindre end et minut"
+ other: "mindre end {{count}} minutter"
+ x_minutes:
+ one: "et minut"
+ other: "{{count}} minutter"
+ about_x_hours:
+ one: "cirka en time"
+ other: "cirka {{count}} timer"
+ x_days:
+ one: "en dag"
+ other: "{{count}} dage"
+ about_x_months:
+ one: "cirka en måned"
+ other: "cirka {{count}} måneder"
+ x_months:
+ one: "en måned"
+ other: "{{count}} måneder"
+ about_x_years:
+ one: "cirka et år"
+ other: "cirka {{count}} år"
+ over_x_years:
+ one: "mere end et år"
+ other: "mere end {{count}} år"
+
+ number:
+ format:
+ separator: ","
+ delimiter: "."
+ precision: 3
+ currency:
+ format:
+ format: "%u %n"
+ unit: "DKK"
+ separator: ","
+ delimiter: "."
+ precision: 2
+ precision:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+ human:
+ format:
+ # separator:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+ percentage:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "er ikke i listen"
+ exclusion: "er reserveret"
+ invalid: "er ikke gyldig"
+ confirmation: "stemmer ikke overens"
+ accepted: "skal accepteres"
+ empty: "må ikke udelades"
+ blank: "skal udfyldes"
+ too_long: "er for lang (højst {{count}} tegn)"
+ too_short: "er for kort (mindst {{count}} tegn)"
+ wrong_length: "har forkert længde (skulle være {{count}} tegn)"
+ taken: "er allerede anvendt"
+ not_a_number: "er ikke et tal"
+ greater_than: "skal være større end {{count}}"
+ greater_than_or_equal_to: "skal være større end eller lig med {{count}}"
+ equal_to: "skal være lig med {{count}}"
+ less_than: "skal være mindre end {{count}}"
+ less_than_or_equal_to: "skal være mindre end eller lig med {{count}}"
+ odd: "skal være ulige"
+ even: "skal være lige"
+ greater_than_start_date: "skal være senere end startdatoen"
+ not_same_project: "hører ikke til samme projekt"
+ circular_dependency: "Denne relation vil skabe et afhængighedsforhold"
+
+ template:
+ header:
+ one: "En fejl forhindrede {{model}} i at blive gemt"
+ other: "{{count}} fejl forhindrede denne {{model}} i at blive gemt"
+ body: "Der var problemer med følgende felter:"
+
+ actionview_instancetag_blank_option: Vælg venligst
+
+ general_text_No: 'Nej'
+ general_text_Yes: 'Ja'
+ general_text_no: 'nej'
+ general_text_yes: 'ja'
+ general_lang_name: 'Danish (Dansk)'
+ general_csv_separator: ','
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Kontoen er opdateret.
+ notice_account_invalid_creditentials: Ugyldig bruger og/eller kodeord
+ notice_account_password_updated: Kodeordet er opdateret.
+ notice_account_wrong_password: Forkert kodeord
+ notice_account_register_done: Kontoen er oprettet. For at aktivere kontoen skal du klikke på linket i den tilsendte email.
+ notice_account_unknown_email: Ukendt bruger.
+ notice_can_t_change_password: Denne konto benytter en ekstern sikkerhedsgodkendelse. Det er ikke muligt at skifte kodeord.
+ notice_account_lost_email_sent: En email med instruktioner til at vælge et nyt kodeord er afsendt til dig.
+ notice_account_activated: Din konto er aktiveret. Du kan nu logge ind.
+ notice_successful_create: Succesfuld oprettelse.
+ notice_successful_update: Succesfuld opdatering.
+ notice_successful_delete: Succesfuld sletning.
+ notice_successful_connection: Succesfuld forbindelse.
+ notice_file_not_found: Siden du forsøger at tilgå eksisterer ikke eller er blevet fjernet.
+ notice_locking_conflict: Data er opdateret af en anden bruger.
+ notice_not_authorized: Du har ikke adgang til denne side.
+ notice_email_sent: "En email er sendt til {{value}}"
+ notice_email_error: "En fejl opstod under afsendelse af email ({{value}})"
+ notice_feeds_access_key_reseted: Din adgangsnøgle til RSS er nulstillet.
+ notice_failed_to_save_issues: "Det mislykkedes at gemme {{count}} sage(r) på {{total}} valgt: {{ids}}."
+ notice_no_issue_selected: "Ingen sag er valgt! Vælg venligst hvilke emner du vil rette."
+ notice_account_pending: "Din konto er oprettet, og afventer administrators godkendelse."
+ notice_default_data_loaded: Standardopsætningen er indlæst.
+
+ error_can_t_load_default_data: "Standardopsætning kunne ikke indlæses: {{value}}"
+ error_scm_not_found: "Adgang nægtet og/eller revision blev ikke fundet i det valgte repository."
+ error_scm_command_failed: "En fejl opstod under forbindelsen til det valgte repository: {{value}}"
+
+ mail_subject_lost_password: "Dit {{value}} kodeord"
+ mail_body_lost_password: 'For at ændre dit kodeord, klik på dette link:'
+ mail_subject_register: "{{value}} kontoaktivering"
+ mail_body_register: 'Klik på dette link for at aktivere din konto:'
+ mail_body_account_information_external: "Du kan bruge din {{value}} konto til at logge ind."
+ mail_body_account_information: Din kontoinformation
+ mail_subject_account_activation_request: "{{value}} kontoaktivering"
+ mail_body_account_activation_request: "En ny bruger ({{value}}) er registreret. Godkend venligst kontoen:"
+
+ gui_validation_error: 1 fejl
+ gui_validation_error_plural: "{{count}} fejl"
+
+ field_name: Navn
+ field_description: Beskrivelse
+ field_summary: Sammenfatning
+ field_is_required: Skal udfyldes
+ field_firstname: Fornavn
+ field_lastname: Efternavn
+ field_mail: Email
+ field_filename: Fil
+ field_filesize: Størrelse
+ field_downloads: Downloads
+ field_author: Forfatter
+ field_created_on: Oprettet
+ field_updated_on: Opdateret
+ field_field_format: Format
+ field_is_for_all: For alle projekter
+ field_possible_values: Mulige værdier
+ field_regexp: Regulære udtryk
+ field_min_length: Mindste længde
+ field_max_length: Største længde
+ field_value: Værdi
+ field_category: Kategori
+ field_title: Titel
+ field_project: Projekt
+ field_issue: Sag
+ field_status: Status
+ field_notes: Noter
+ field_is_closed: Sagen er lukket
+ field_is_default: Standardværdi
+ field_tracker: Type
+ field_subject: Emne
+ field_due_date: Deadline
+ field_assigned_to: Tildelt til
+ field_priority: Prioritet
+ field_fixed_version: Target version
+ field_user: Bruger
+ field_role: Rolle
+ field_homepage: Hjemmeside
+ field_is_public: Offentlig
+ field_parent: Underprojekt af
+ field_is_in_chlog: Sager vist i ændringer
+ field_is_in_roadmap: Sager vist i roadmap
+ field_login: Login
+ field_mail_notification: Email-påmindelser
+ field_admin: Administrator
+ field_last_login_on: Sidste forbindelse
+ field_language: Sprog
+ field_effective_date: Dato
+ field_password: Kodeord
+ field_new_password: Nyt kodeord
+ field_password_confirmation: Bekræft
+ field_version: Version
+ field_type: Type
+ field_host: Vært
+ field_port: Port
+ field_account: Kode
+ field_base_dn: Base DN
+ field_attr_login: Login attribut
+ field_attr_firstname: Fornavn attribut
+ field_attr_lastname: Efternavn attribut
+ field_attr_mail: Email attribut
+ field_onthefly: løbende brugeroprettelse
+ field_start_date: Start
+ field_done_ratio: % Færdig
+ field_auth_source: Sikkerhedsmetode
+ field_hide_mail: Skjul min email
+ field_comments: Kommentar
+ field_url: URL
+ field_start_page: Startside
+ field_subproject: Underprojekt
+ field_hours: Timer
+ field_activity: Aktivitet
+ field_spent_on: Dato
+ field_identifier: Identifikator
+ field_is_filter: Brugt som et filter
+ field_issue_to: Beslægtede sag
+ field_delay: Udsættelse
+ field_assignable: Sager kan tildeles denne rolle
+ field_redirect_existing_links: Videresend eksisterende links
+ field_estimated_hours: Anslået tid
+ field_column_names: Kolonner
+ field_time_zone: Tidszone
+ field_searchable: Søgbar
+ field_default_value: Standardværdi
+
+ setting_app_title: Applikationstitel
+ setting_app_subtitle: Applikationsundertekst
+ setting_welcome_text: Velkomsttekst
+ setting_default_language: Standardsporg
+ setting_login_required: Sikkerhed påkrævet
+ setting_self_registration: Brugeroprettelse
+ setting_attachment_max_size: Vedhæftede filers max størrelse
+ setting_issues_export_limit: Sagseksporteringsbegrænsning
+ setting_mail_from: Afsender-email
+ setting_bcc_recipients: Skjult modtager (bcc)
+ setting_host_name: Værtsnavn
+ setting_text_formatting: Tekstformatering
+ setting_wiki_compression: Komprimering af wiki-historik
+ setting_feeds_limit: Feed indholdsbegrænsning
+ setting_autofetch_changesets: Hent automatisk commits
+ setting_sys_api_enabled: Aktiver webservice for automatisk administration af repository
+ setting_commit_ref_keywords: Referencenøgleord
+ setting_commit_fix_keywords: Afslutningsnøgleord
+ setting_autologin: Automatisk login
+ setting_date_format: Datoformat
+ setting_time_format: Tidsformat
+ setting_cross_project_issue_relations: Tillad sagsrelationer på tværs af projekter
+ setting_issue_list_default_columns: Standardkolonner på sagslisten
+ setting_repositories_encodings: Repository-tegnsæt
+ setting_emails_footer: Email-fodnote
+ setting_protocol: Protokol
+ setting_user_format: Brugervisningsformat
+
+ project_module_issue_tracking: Sagssøgning
+ project_module_time_tracking: Tidsstyring
+ project_module_news: Nyheder
+ project_module_documents: Dokumenter
+ project_module_files: Filer
+ project_module_wiki: Wiki
+ project_module_repository: Repository
+ project_module_boards: Fora
+
+ label_user: Bruger
+ label_user_plural: Brugere
+ label_user_new: Ny bruger
+ label_project: Projekt
+ label_project_new: Nyt projekt
+ label_project_plural: Projekter
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Alle projekter
+ label_project_latest: Seneste projekter
+ label_issue: Sag
+ label_issue_new: Opret sag
+ label_issue_plural: Sager
+ label_issue_view_all: Vis alle sager
+ label_issues_by: "Sager fra {{value}}"
+ label_issue_added: Sagen er oprettet
+ label_issue_updated: Sagen er opdateret
+ label_document: Dokument
+ label_document_new: Nyt dokument
+ label_document_plural: Dokumenter
+ label_document_added: Dokument tilføjet
+ label_role: Rolle
+ label_role_plural: Roller
+ label_role_new: Ny rolle
+ label_role_and_permissions: Roller og rettigheder
+ label_member: Medlem
+ label_member_new: Nyt medlem
+ label_member_plural: Medlemmer
+ label_tracker: Type
+ label_tracker_plural: Typer
+ label_tracker_new: Ny type
+ label_workflow: Arbejdsgang
+ label_issue_status: Sagsstatus
+ label_issue_status_plural: Sagsstatuser
+ label_issue_status_new: Ny status
+ label_issue_category: Sagskategori
+ label_issue_category_plural: Sagskategorier
+ label_issue_category_new: Ny kategori
+ label_custom_field: Brugerdefineret felt
+ label_custom_field_plural: Brugerdefinerede felter
+ label_custom_field_new: Nyt brugerdefineret felt
+ label_enumerations: Værdier
+ label_enumeration_new: Ny værdi
+ label_information: Information
+ label_information_plural: Information
+ label_please_login: Login
+ label_register: Registrér
+ label_password_lost: Glemt kodeord
+ label_home: Forside
+ label_my_page: Min side
+ label_my_account: Min konto
+ label_my_projects: Mine projekter
+ label_administration: Administration
+ label_login: Log ind
+ label_logout: Log ud
+ label_help: Hjælp
+ label_reported_issues: Rapporterede sager
+ label_assigned_to_me_issues: Sager tildelt mig
+ label_last_login: Sidste login tidspunkt
+ label_registered_on: Registeret den
+ label_activity: Aktivitet
+ label_new: Ny
+ label_logged_as: Registreret som
+ label_environment: Miljø
+ label_authentication: Sikkerhed
+ label_auth_source: Sikkerhedsmetode
+ label_auth_source_new: Ny sikkerhedsmetode
+ label_auth_source_plural: Sikkerhedsmetoder
+ label_subproject_plural: Underprojekter
+ label_min_max_length: Min - Max længde
+ label_list: Liste
+ label_date: Dato
+ label_integer: Heltal
+ label_float: Kommatal
+ label_boolean: Sand/falsk
+ label_string: Tekst
+ label_text: Lang tekst
+ label_attribute: Attribut
+ label_attribute_plural: Attributter
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Ingen data at vise
+ label_change_status: Ændringsstatus
+ label_history: Historik
+ label_attachment: Fil
+ label_attachment_new: Ny fil
+ label_attachment_delete: Slet fil
+ label_attachment_plural: Filer
+ label_file_added: Fil tilføjet
+ label_report: Rapport
+ label_report_plural: Rapporter
+ label_news: Nyheder
+ label_news_new: Tilføj nyheder
+ label_news_plural: Nyheder
+ label_news_latest: Seneste nyheder
+ label_news_view_all: Vis alle nyheder
+ label_news_added: Nyhed tilføjet
+ label_change_log: Ændringer
+ label_settings: Indstillinger
+ label_overview: Oversigt
+ label_version: Udgave
+ label_version_new: Ny udgave
+ label_version_plural: Udgaver
+ label_confirmation: Bekræftelser
+ label_export_to: Eksporter til
+ label_read: Læs...
+ label_public_projects: Offentlige projekter
+ label_open_issues: åben
+ label_open_issues_plural: åbne
+ label_closed_issues: lukket
+ label_closed_issues_plural: lukkede
+ label_x_open_issues_abbr_on_total:
+ zero: 0 åbne / {{total}}
+ one: 1 åben / {{total}}
+ other: "{{count}} åbne / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 åbne
+ one: 1 åben
+ other: "{{count}} åbne"
+ label_x_closed_issues_abbr:
+ zero: 0 lukkede
+ one: 1 lukket
+ other: "{{count}} lukkede"
+ label_total: Total
+ label_permissions: Rettigheder
+ label_current_status: Nuværende status
+ label_new_statuses_allowed: Ny status tilladt
+ label_all: alle
+ label_none: intet
+ label_nobody: ingen
+ label_next: Næste
+ label_previous: Forrig
+ label_used_by: Brugt af
+ label_details: Detaljer
+ label_add_note: Tilføj note
+ label_per_page: Pr. side
+ label_calendar: Kalender
+ label_months_from: måneder frem
+ label_gantt: Gantt
+ label_internal: Intern
+ label_last_changes: "sidste {{count}} ændringer"
+ label_change_view_all: Vis alle ændringer
+ label_personalize_page: Tilret denne side
+ label_comment: Kommentar
+ label_comment_plural: Kommentarer
+ label_x_comments:
+ zero: ingen kommentarer
+ one: 1 kommentar
+ other: "{{count}} kommentarer"
+ label_comment_add: Tilføj en kommentar
+ label_comment_added: Kommentaren er tilføjet
+ label_comment_delete: Slet kommentar
+ label_query: Brugerdefineret forespørgsel
+ label_query_plural: Brugerdefinerede forespørgsler
+ label_query_new: Ny forespørgsel
+ label_filter_add: Tilføj filter
+ label_filter_plural: Filtre
+ label_equals: er
+ label_not_equals: er ikke
+ label_in_less_than: er mindre end
+ label_in_more_than: er større end
+ label_in: indeholdt i
+ label_today: i dag
+ label_all_time: altid
+ label_yesterday: i går
+ label_this_week: denne uge
+ label_last_week: sidste uge
+ label_last_n_days: "sidste {{count}} dage"
+ label_this_month: denne måned
+ label_last_month: sidste måned
+ label_this_year: dette år
+ label_date_range: Dato interval
+ label_less_than_ago: mindre end dage siden
+ label_more_than_ago: mere end dage siden
+ label_ago: days siden
+ label_contains: indeholder
+ label_not_contains: ikke indeholder
+ label_day_plural: dage
+ label_repository: Repository
+ label_repository_plural: Repositories
+ label_browse: Gennemse
+ label_modification: "{{count}} ændring"
+ label_modification_plural: "{{count}} ændringer"
+ label_revision: Revision
+ label_revision_plural: Revisioner
+ label_associated_revisions: Tilnyttede revisioner
+ label_added: tilføjet
+ label_modified: ændret
+ label_deleted: slettet
+ label_latest_revision: Seneste revision
+ label_latest_revision_plural: Seneste revisioner
+ label_view_revisions: Se revisioner
+ label_max_size: Maksimal størrelse
+ label_sort_highest: Flyt til toppen
+ label_sort_higher: Flyt op
+ label_sort_lower: Flyt ned
+ label_sort_lowest: Flyt til bunden
+ label_roadmap: Roadmap
+ label_roadmap_due_in: Deadline
+ label_roadmap_overdue: "{{value}} forsinket"
+ label_roadmap_no_issues: Ingen sager i denne version
+ label_search: Søg
+ label_result_plural: Resultater
+ label_all_words: Alle ord
+ label_wiki: Wiki
+ label_wiki_edit: Wiki ændring
+ label_wiki_edit_plural: Wiki ændringer
+ label_wiki_page: Wiki side
+ label_wiki_page_plural: Wiki sider
+ label_index_by_title: Indhold efter titel
+ label_index_by_date: Indhold efter dato
+ label_current_version: Nuværende version
+ label_preview: Forhåndsvisning
+ label_feed_plural: Feeds
+ label_changes_details: Detaljer for alle ændringer
+ label_issue_tracking: Sags søgning
+ label_spent_time: Anvendt tid
+ label_f_hour: "{{value}} time"
+ label_f_hour_plural: "{{value}} timer"
+ label_time_tracking: Tidsstyring
+ label_change_plural: Ændringer
+ label_statistics: Statistik
+ label_commits_per_month: Commits pr. måned
+ label_commits_per_author: Commits pr. bruger
+ label_view_diff: Vis forskelle
+ label_diff_inline: inline
+ label_diff_side_by_side: side om side
+ label_options: Optioner
+ label_copy_workflow_from: Kopier arbejdsgang fra
+ label_permissions_report: Godkendelsesrapport
+ label_watched_issues: Overvågede sager
+ label_related_issues: Relaterede sager
+ label_applied_status: Anvendte statuser
+ label_loading: Indlæser...
+ label_relation_new: Ny relation
+ label_relation_delete: Slet relation
+ label_relates_to: relaterer til
+ label_duplicates: kopierer
+ label_blocks: blokerer
+ label_blocked_by: blokeret af
+ label_precedes: kommer før
+ label_follows: følger
+ label_end_to_start: slut til start
+ label_end_to_end: slut til slut
+ label_start_to_start: start til start
+ label_start_to_end: start til slut
+ label_stay_logged_in: Forbliv indlogget
+ label_disabled: deaktiveret
+ label_show_completed_versions: Vis færdige versioner
+ label_me: mig
+ label_board: Forum
+ label_board_new: Nyt forum
+ label_board_plural: Fora
+ label_topic_plural: Emner
+ label_message_plural: Beskeder
+ label_message_last: Sidste besked
+ label_message_new: Ny besked
+ label_message_posted: Besked tilføjet
+ label_reply_plural: Besvarer
+ label_send_information: Send konto information til bruger
+ label_year: År
+ label_month: Måned
+ label_week: Uge
+ label_date_from: Fra
+ label_date_to: Til
+ label_language_based: Baseret på brugerens sprog
+ label_sort_by: "Sortér efter {{value}}"
+ label_send_test_email: Send en test email
+ label_feeds_access_key_created_on: "RSS adgangsnøgle dannet for {{value}} siden"
+ label_module_plural: Moduler
+ label_added_time_by: "Tilføjet af {{author}} for {{age}} siden"
+ label_updated_time: "Opdateret for {{value}} siden"
+ label_jump_to_a_project: Skift til projekt...
+ label_file_plural: Filer
+ label_changeset_plural: Ændringer
+ label_default_columns: Standard kolonner
+ label_no_change_option: (Ingen ændringer)
+ label_bulk_edit_selected_issues: Masse-ret de valgte sager
+ label_theme: Tema
+ label_default: standard
+ label_search_titles_only: Søg kun i titler
+ label_user_mail_option_all: "For alle hændelser på mine projekter"
+ label_user_mail_option_selected: "For alle hændelser på de valgte projekter..."
+ label_user_mail_option_none: "Kun for ting jeg overvåger eller jeg er involveret i"
+ label_user_mail_no_self_notified: "Jeg ønsker ikke besked om ændring foretaget af mig selv"
+ label_registration_activation_by_email: kontoaktivering på email
+ label_registration_manual_activation: manuel kontoaktivering
+ label_registration_automatic_activation: automatisk kontoaktivering
+ label_display_per_page: "Per side: {{value}}"
+ label_age: Alder
+ label_change_properties: Ændre indstillinger
+ label_general: Generelt
+ label_more: Mere
+ label_scm: SCM
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP-godkendelse
+ label_downloads_abbr: D/L
+
+ button_login: Login
+ button_submit: Send
+ button_save: Gem
+ button_check_all: Vælg alt
+ button_uncheck_all: Fravælg alt
+ button_delete: Slet
+ button_create: Opret
+ button_test: Test
+ button_edit: Ret
+ button_add: Tilføj
+ button_change: Ændre
+ button_apply: Anvend
+ button_clear: Nulstil
+ button_lock: Lås
+ button_unlock: Lås op
+ button_download: Download
+ button_list: List
+ button_view: Vis
+ button_move: Flyt
+ button_back: Tilbage
+ button_cancel: Annullér
+ button_activate: Aktivér
+ button_sort: Sortér
+ button_log_time: Log tid
+ button_rollback: Tilbagefør til denne version
+ button_watch: Overvåg
+ button_unwatch: Stop overvågning
+ button_reply: Besvar
+ button_archive: Arkivér
+ button_unarchive: Fjern fra arkiv
+ button_reset: Nulstil
+ button_rename: Omdøb
+ button_change_password: Skift kodeord
+ button_copy: Kopiér
+ button_annotate: Annotér
+ button_update: Opdatér
+ button_configure: Konfigurér
+
+ status_active: aktiv
+ status_registered: registreret
+ status_locked: låst
+
+ text_select_mail_notifications: Vælg handlinger der skal sendes email besked for.
+ text_regexp_info: f.eks. ^[A-ZÆØÅ0-9]+$
+ text_min_max_length_info: 0 betyder ingen begrænsninger
+ text_project_destroy_confirmation: Er du sikker på at du vil slette dette projekt og alle relaterede data?
+ text_workflow_edit: Vælg en rolle samt en type, for at redigere arbejdsgangen
+ text_are_you_sure: Er du sikker?
+ text_tip_task_begin_day: opgaven begynder denne dag
+ text_tip_task_end_day: opaven slutter denne dag
+ text_tip_task_begin_end_day: opgaven begynder og slutter denne dag
+ text_project_identifier_info: 'Små bogstaver (a-z), numre og bindestreg er tilladt.<br />Denne er en unik identifikation for projektet, og kan defor ikke rettes senere.'
+ text_caracters_maximum: "max {{count}} karakterer."
+ text_caracters_minimum: "Skal være mindst {{count}} karakterer lang."
+ text_length_between: "Længde skal være mellem {{min}} og {{max}} karakterer."
+ text_tracker_no_workflow: Ingen arbejdsgang defineret for denne type
+ text_unallowed_characters: Ikke-tilladte karakterer
+ text_comma_separated: Adskillige værdier tilladt (adskilt med komma).
+ text_issues_ref_in_commit_messages: Referer og løser sager i commit-beskeder
+ text_issue_added: "Sag {{id}} er rapporteret af {{author}}."
+ text_issue_updated: "Sag {{id}} er blevet opdateret af {{author}}."
+ text_wiki_destroy_confirmation: Er du sikker på at du vil slette denne wiki og dens indhold?
+ text_issue_category_destroy_question: "Nogle sager ({{count}}) er tildelt denne kategori. Hvad ønsker du at gøre?"
+ text_issue_category_destroy_assignments: Slet tildelinger af kategori
+ text_issue_category_reassign_to: Tildel sager til denne kategori
+ text_user_mail_option: "For ikke-valgte projekter vil du kun modtage beskeder omhandlende ting du er involveret i eller overvåger (f.eks. sager du har indberettet eller ejer)."
+ text_no_configuration_data: "Roller, typer, sagsstatuser og arbejdsgange er endnu ikke konfigureret.\nDet er anbefalet at indlæse standardopsætningen. Du vil kunne ændre denne når den er indlæst."
+ text_load_default_configuration: Indlæs standardopsætningen
+ text_status_changed_by_changeset: "Anvendt i ændring {{value}}."
+ text_issues_destroy_confirmation: 'Er du sikker på du ønsker at slette den/de valgte sag(er)?'
+ text_select_project_modules: 'Vælg moduler er skal være aktiveret for dette projekt:'
+ text_default_administrator_account_changed: Standard administratorkonto ændret
+ text_file_repository_writable: Filarkiv er skrivbar
+ text_rmagick_available: RMagick tilgængelig (valgfri)
+
+ default_role_manager: Leder
+ default_role_developper: Udvikler
+ default_role_reporter: Rapportør
+ default_tracker_bug: Bug
+ default_tracker_feature: Feature
+ default_tracker_support: Support
+ default_issue_status_new: Ny
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Løst
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Lukket
+ default_issue_status_rejected: Afvist
+ default_doc_category_user: Brugerdokumentation
+ default_doc_category_tech: Teknisk dokumentation
+ default_priority_low: Lav
+ default_priority_normal: Normal
+ default_priority_high: Høj
+ default_priority_urgent: Akut
+ default_priority_immediate: Omgående
+ default_activity_design: Design
+ default_activity_development: Udvikling
+
+ enumeration_issue_priorities: Sagsprioriteter
+ enumeration_doc_categories: Dokumentkategorier
+ enumeration_activities: Aktiviteter (tidsstyring)
+
+ label_add_another_file: Tilføj endnu en fil
+ label_chronological_order: I kronologisk rækkefølge
+ setting_activity_days_default: Antal dage der vises under projektaktivitet
+ text_destroy_time_entries_question: "{{hours}} timer er registreret på denne sag som du er ved at slette. Hvad vil du gøre?"
+ error_issue_not_found_in_project: 'Sagen blev ikke fundet eller tilhører ikke dette projekt'
+ text_assign_time_entries_to_project: Tildel raporterede timer til projektet
+ setting_display_subprojects_issues: Vis sager for underprojekter på hovedprojektet som standard
+ label_optional_description: Optionel beskrivelse
+ text_destroy_time_entries: Slet registrerede timer
+ field_comments_sorting: Vis kommentar
+ text_reassign_time_entries: 'Tildel registrerede timer til denne sag igen'
+ label_reverse_chronological_order: I omvendt kronologisk rækkefølge
+ label_preferences: Preferences
+ label_overall_activity: Overordnet aktivitet
+ setting_default_projects_public: Nye projekter er offentlige som standard
+ error_scm_annotate: "Filen findes ikke, eller kunne ikke annoteres."
+ label_planning: Planlægning
+ text_subprojects_destroy_warning: "Dets underprojekter(er): {{value}} vil også blive slettet."
+ permission_edit_issues: Redigér sager
+ setting_diff_max_lines_displayed: Højeste antal forskelle der vises
+ permission_edit_own_issue_notes: Redigér egne noter
+ setting_enabled_scm: Aktiveret SCM
+ button_quote: Citér
+ permission_view_files: Se filer
+ permission_add_issues: Tilføj sager
+ permission_edit_own_messages: Redigér egne beskeder
+ permission_delete_own_messages: Slet egne beskeder
+ permission_manage_public_queries: Administrér offentlig forespørgsler
+ permission_log_time: Registrér anvendt tid
+ label_renamed: omdøbt
+ label_incoming_emails: Indkommende emails
+ permission_view_changesets: Se ændringer
+ permission_manage_versions: Administrér versioner
+ permission_view_time_entries: Se anvendt tid
+ label_generate_key: Generér en nøglefil
+ permission_manage_categories: Administrér sagskategorier
+ permission_manage_wiki: Administrér wiki
+ setting_sequential_project_identifiers: Generér sekventielle projekt-identifikatorer
+ setting_plain_text_mail: Emails som almindelig tekst (ingen HTML)
+ field_parent_title: Siden over
+ text_email_delivery_not_configured: "Email-afsendelse er ikke indstillet og notifikationer er defor slået fra.\nKonfigurér din SMTP server i config/email.yml og genstart applikationen for at aktivere email-afsendelse."
+ permission_protect_wiki_pages: Beskyt wiki sider
+ permission_manage_documents: Administrér dokumenter
+ permission_add_issue_watchers: Tilføj overvågere
+ warning_attachments_not_saved: "der var {{count}} fil(er), som ikke kunne gemmes."
+ permission_comment_news: Kommentér nyheder
+ text_enumeration_category_reassign_to: 'FLyt dem til denne værdi:'
+ permission_select_project_modules: Vælg projektmoduler
+ permission_view_gantt: Se Gantt diagram
+ permission_delete_messages: Slet beskeder
+ permission_move_issues: Flyt sager
+ permission_edit_wiki_pages: Redigér wiki sider
+ label_user_activity: "{{value}}'s aktivitet"
+ permission_manage_issue_relations: Administrér sags-relationer
+ label_issue_watchers: Overvågere
+ permission_delete_wiki_pages: Slet wiki sider
+ notice_unable_delete_version: Kan ikke slette versionen.
+ permission_view_wiki_edits: Se wiki historik
+ field_editable: Redigérbar
+ label_duplicated_by: dubleret af
+ permission_manage_boards: Administrér fora
+ permission_delete_wiki_pages_attachments: Slet filer vedhæftet wiki sider
+ permission_view_messages: Se beskeder
+ text_enumeration_destroy_question: "{{count}} objekter er tildelt denne værdi."
+ permission_manage_files: Administrér filer
+ permission_add_messages: Opret beskeder
+ permission_edit_issue_notes: Redigér noter
+ permission_manage_news: Administrér nyheder
+ text_plugin_assets_writable: Der er skriverettigheder til plugin assets folderen
+ label_display: Vis
+ label_and_its_subprojects: "{{value}} og dets underprojekter"
+ permission_view_calendar: Se kalender
+ button_create_and_continue: Opret og fortsæt
+ setting_gravatar_enabled: Anvend Gravatar bruger ikoner
+ label_updated_time_by: "Opdateret af {{author}} for {{age}} siden"
+ text_diff_truncated: '... Listen over forskelle er bleve afkortet da den overstiger den maksimale størrelse der kan vises.'
+ text_user_wrote: "{{value}} skrev:"
+ setting_mail_handler_api_enabled: Aktiver webservice for indkomne emails
+ permission_delete_issues: Slet sager
+ permission_view_documents: Se dokumenter
+ permission_browse_repository: Gennemse repository
+ permission_manage_repository: Administrér repository
+ permission_manage_members: Administrér medlemmer
+ mail_subject_reminder: "{{count}} sag(er) har deadline i de kommende dage"
+ permission_add_issue_notes: Tilføj noter
+ permission_edit_messages: Redigér beskeder
+ permission_view_issue_watchers: Se liste over overvågere
+ permission_commit_access: Commit adgang
+ setting_mail_handler_api_key: API nøgle
+ label_example: Eksempel
+ permission_rename_wiki_pages: Omdøb wiki sider
+ text_custom_field_possible_values_info: 'En linje for hver værdi'
+ permission_view_wiki_pages: Se wiki
+ permission_edit_project: Redigér projekt
+ permission_save_queries: Gem forespørgsler
+ label_copied: kopieret
+ setting_commit_logs_encoding: Kodning af Commit beskeder
+ text_repository_usernames_mapping: "Vælg eller opdatér de Redmine brugere der svarer til de enkelte brugere fundet i repository loggen.\nBrugere med samme brugernavn eller email adresse i både Redmine og det valgte repository bliver automatisk koblet sammen."
+ permission_edit_time_entries: Redigér tidsregistreringer
+ general_csv_decimal_separator: ','
+ permission_edit_own_time_entries: Redigér egne tidsregistreringer
+ setting_repository_log_display_limit: Højeste antal revisioner vist i fil-log
+ setting_file_max_size_displayed: Maksimale størrelse på tekstfiler vist inline
+ field_watcher: Overvåger
+ setting_openid: Tillad OpenID login og registrering
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: eller login med OpenID
+ setting_per_page_options: Enheder per side muligheder
+ mail_body_reminder: "{{count}} sage(er) som er tildelt dig har deadline indenfor de næste {{days}} dage:"
+ field_content: Indhold
+ label_descending: Aftagende
+ label_sort: Sortér
+ label_ascending: Tiltagende
+ label_date_from_to: Fra {{start}} til {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Denne side har {{descendants}} underside(r) og afledte. Hvad vil du gøre?
+ text_wiki_page_reassign_children: Flyt undersider til denne side
+ text_wiki_page_nullify_children: Behold undersider som rod-sider
+ text_wiki_page_destroy_children: Slet undersider ogalle deres afledte sider.
+ setting_password_min_length: Mindste længde på kodeord
+ field_group_by: Gruppér resultater efter
+ mail_subject_wiki_content_updated: "'{{page}}' wikisiden er blevet opdateret"
+ label_wiki_content_added: Wiki side tilføjet
+ mail_subject_wiki_content_added: "'{{page}}' wikisiden er blevet tilføjet"
+ mail_body_wiki_content_added: The '{{page}}' wikiside er blevet tilføjet af {{author}}.
+ label_wiki_content_updated: Wikiside opdateret
+ mail_body_wiki_content_updated: Wikisiden '{{page}}' er blevet opdateret af {{author}}.
+ permission_add_project: Opret projekt
+ setting_new_project_user_role_id: Denne rolle gives til en bruger, som ikke er administrator, og som opretter et projekt
+ label_view_all_revisions: Se alle revisioner
+ label_tag: Tag
+ label_branch: Branch
+ error_no_tracker_in_project: Der er ingen sagshåndtering for dette projekt. Kontrollér venligst projektindstillingerne.
+ error_no_default_issue_status: Der er ikek defineret en standardstatus. Kontrollér venligst indstillingernen (Gå til "Administration -> Sagsstatuser").
+ text_journal_changed: "{{label}} ændret fra {{old}} til {{new}}"
+ text_journal_set_to: "{{label}} sat til {{value}}"
+ text_journal_deleted: "{{label}} slettet ({{old}})"
+ label_group_plural: Grupper
+ label_group: Grupper
+ label_group_new: Ny gruppe
+ label_time_entry_plural: Anvendt tid
+ text_journal_added: "{{label}} {{value}} 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
--- /dev/null
+# German translations for Ruby on Rails
+# by Clemens Kofler (clemens@railway.at)
+
+de:
+ date:
+ formats:
+ default: "%d.%m.%Y"
+ short: "%e. %b"
+ long: "%e. %B %Y"
+ only_day: "%e"
+
+ day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag]
+ abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa]
+ month_names: [~, Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember]
+ abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%A, %e. %B %Y, %H:%M Uhr"
+ short: "%e. %B, %H:%M Uhr"
+ long: "%A, %e. %B %Y, %H:%M Uhr"
+ time: "%H:%M"
+
+ am: "vormittags"
+ pm: "nachmittags"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'eine halbe Minute'
+ less_than_x_seconds:
+ zero: 'weniger als 1 Sekunde'
+ one: 'weniger als 1 Sekunde'
+ other: 'weniger als {{count}} Sekunden'
+ x_seconds:
+ one: '1 Sekunde'
+ other: '{{count}} Sekunden'
+ less_than_x_minutes:
+ zero: 'weniger als 1 Minute'
+ one: 'weniger als eine Minute'
+ other: 'weniger als {{count}} Minuten'
+ x_minutes:
+ one: '1 Minute'
+ other: '{{count}} Minuten'
+ about_x_hours:
+ one: 'etwa 1 Stunde'
+ other: 'etwa {{count}} Stunden'
+ x_days:
+ one: '1 Tag'
+ other: '{{count}} Tage'
+ about_x_months:
+ one: 'etwa 1 Monat'
+ other: 'etwa {{count}} Monate'
+ x_months:
+ one: '1 Monat'
+ other: '{{count}} Monate'
+ about_x_years:
+ one: 'etwa 1 Jahr'
+ other: 'etwa {{count}} Jahre'
+ over_x_years:
+ one: 'mehr als 1 Jahr'
+ other: 'mehr als {{count}} Jahre'
+
+ number:
+ format:
+ precision: 2
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: '€'
+ format: '%n%u'
+ separator:
+ delimiter:
+ precision:
+ percentage:
+ format:
+ delimiter: ""
+ precision:
+ format:
+ delimiter: ""
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ sentence_connector: "und"
+ skip_last_comma: true
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Konnte dieses {{model}} Objekt nicht speichern: 1 Fehler."
+ other: "Konnte dieses {{model}} Objekt nicht speichern: {{count}} Fehler."
+ body: "Bitte überprüfen Sie die folgenden Felder:"
+
+ messages:
+ inclusion: "ist kein gültiger Wert"
+ exclusion: "ist nicht verfügbar"
+ invalid: "ist nicht gültig"
+ confirmation: "stimmt nicht mit der Bestätigung überein"
+ accepted: "muss akzeptiert werden"
+ empty: "muss ausgefüllt werden"
+ blank: "muss ausgefüllt werden"
+ too_long: "ist zu lang (nicht mehr als {{count}} Zeichen)"
+ too_short: "ist zu kurz (nicht weniger als {{count}} Zeichen)"
+ wrong_length: "hat die falsche Länge (muss genau {{count}} Zeichen haben)"
+ taken: "ist bereits vergeben"
+ not_a_number: "ist keine Zahl"
+ greater_than: "muss größer als {{count}} sein"
+ greater_than_or_equal_to: "muss größer oder gleich {{count}} sein"
+ equal_to: "muss genau {{count}} sein"
+ less_than: "muss kleiner als {{count}} sein"
+ less_than_or_equal_to: "muss kleiner oder gleich {{count}} sein"
+ odd: "muss ungerade sein"
+ even: "muss gerade sein"
+ greater_than_start_date: "muss größer als Anfangsdatum sein"
+ not_same_project: "gehört nicht zum selben Projekt"
+ circular_dependency: "Diese Beziehung würde eine zyklische Abhängigkeit erzeugen"
+ models:
+
+ actionview_instancetag_blank_option: Bitte auswählen
+
+ general_text_No: 'Nein'
+ general_text_Yes: 'Ja'
+ general_text_no: 'nein'
+ general_text_yes: 'ja'
+ general_lang_name: 'Deutsch'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Konto wurde erfolgreich aktualisiert.
+ notice_account_invalid_creditentials: Benutzer oder Kennwort unzulässig
+ notice_account_password_updated: Kennwort wurde erfolgreich aktualisiert.
+ notice_account_wrong_password: Falsches Kennwort
+ notice_account_register_done: Konto wurde erfolgreich angelegt.
+ notice_account_unknown_email: Unbekannter Benutzer.
+ notice_can_t_change_password: Dieses Konto verwendet eine externe Authentifizierungs-Quelle. Unmöglich, das Kennwort zu ändern.
+ notice_account_lost_email_sent: Eine E-Mail mit Anweisungen, ein neues Kennwort zu wählen, wurde Ihnen geschickt.
+ notice_account_activated: Ihr Konto ist aktiviert. Sie können sich jetzt anmelden.
+ notice_successful_create: Erfolgreich angelegt
+ notice_successful_update: Erfolgreich aktualisiert.
+ notice_successful_delete: Erfolgreich gelöscht.
+ notice_successful_connection: Verbindung erfolgreich.
+ notice_file_not_found: Anhang existiert nicht oder ist gelöscht worden.
+ notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
+ notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen.
+ notice_email_sent: "Eine E-Mail wurde an {{value}} gesendet."
+ notice_email_error: "Beim Senden einer E-Mail ist ein Fehler aufgetreten ({{value}})."
+ notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt.
+ notice_failed_to_save_issues: "{{count}} von {{total}} ausgewählten Tickets konnte(n) nicht gespeichert werden: {{ids}}."
+ notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten."
+ notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators."
+ notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen.
+ notice_unable_delete_version: Die Version konnte nicht gelöscht werden
+
+ error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: {{value}}"
+ error_scm_not_found: Eintrag und/oder Revision existiert nicht im Projektarchiv.
+ error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: {{value}}"
+ error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden."
+ error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehört nicht zu diesem Projekt.'
+
+ warning_attachments_not_saved: "{{count}} Datei(en) konnten nicht gespeichert werden."
+
+ mail_subject_lost_password: "Ihr {{value}} Kennwort"
+ mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:'
+ mail_subject_register: "{{value}} Kontoaktivierung"
+ mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:'
+ mail_body_account_information_external: "Sie können sich mit Ihrem Konto {{value}} an anmelden."
+ mail_body_account_information: Ihre Konto-Informationen
+ mail_subject_account_activation_request: "Antrag auf {{value}} Kontoaktivierung"
+ mail_body_account_activation_request: "Ein neuer Benutzer ({{value}}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
+ mail_subject_reminder: "{{count}} Tickets müssen in den nächsten Tagen abgegeben werden"
+ mail_body_reminder: "{{count}} Tickets, die Ihnen zugewiesen sind, müssen in den nächsten {{days}} Tagen abgegeben werden:"
+
+ gui_validation_error: 1 Fehler
+ gui_validation_error_plural: "{{count}} Fehler"
+
+ field_name: Name
+ field_description: Beschreibung
+ field_summary: Zusammenfassung
+ field_is_required: Erforderlich
+ field_firstname: Vorname
+ field_lastname: Nachname
+ field_mail: E-Mail
+ field_filename: Datei
+ field_filesize: Größe
+ field_downloads: Downloads
+ field_author: Autor
+ field_created_on: Angelegt
+ field_updated_on: Aktualisiert
+ field_field_format: Format
+ field_is_for_all: Für alle Projekte
+ field_possible_values: Mögliche Werte
+ field_regexp: Regulärer Ausdruck
+ field_min_length: Minimale Länge
+ field_max_length: Maximale Länge
+ field_value: Wert
+ field_category: Kategorie
+ field_title: Titel
+ field_project: Projekt
+ field_issue: Ticket
+ field_status: Status
+ field_notes: Kommentare
+ field_is_closed: Ticket geschlossen
+ field_is_default: Standardeinstellung
+ field_tracker: Tracker
+ field_subject: Thema
+ field_due_date: Abgabedatum
+ field_assigned_to: Zugewiesen an
+ field_priority: Priorität
+ field_fixed_version: Zielversion
+ field_user: Benutzer
+ field_role: Rolle
+ field_homepage: Projekt-Homepage
+ field_is_public: Öffentlich
+ field_parent: Unterprojekt von
+ field_is_in_chlog: Im Change-Log anzeigen
+ field_is_in_roadmap: In der Roadmap anzeigen
+ field_login: Mitgliedsname
+ field_mail_notification: Mailbenachrichtigung
+ field_admin: Administrator
+ field_last_login_on: Letzte Anmeldung
+ field_language: Sprache
+ field_effective_date: Datum
+ field_password: Kennwort
+ field_new_password: Neues Kennwort
+ field_password_confirmation: Bestätigung
+ field_version: Version
+ field_type: Typ
+ field_host: Host
+ field_port: Port
+ field_account: Konto
+ field_base_dn: Base DN
+ field_attr_login: Mitgliedsname-Attribut
+ field_attr_firstname: Vorname-Attribut
+ field_attr_lastname: Name-Attribut
+ field_attr_mail: E-Mail-Attribut
+ field_onthefly: On-the-fly-Benutzererstellung
+ field_start_date: Beginn
+ field_done_ratio: % erledigt
+ field_auth_source: Authentifizierungs-Modus
+ field_hide_mail: E-Mail-Adresse nicht anzeigen
+ field_comments: Kommentar
+ field_url: URL
+ field_start_page: Hauptseite
+ field_subproject: Subprojekt von
+ field_hours: Stunden
+ field_activity: Aktivität
+ field_spent_on: Datum
+ field_identifier: Kennung
+ field_is_filter: Als Filter benutzen
+ field_issue_to: Zugehöriges Ticket
+ field_delay: Pufferzeit
+ field_assignable: Tickets können dieser Rolle zugewiesen werden
+ field_redirect_existing_links: Existierende Links umleiten
+ field_estimated_hours: Geschätzter Aufwand
+ field_column_names: Spalten
+ field_time_zone: Zeitzone
+ field_searchable: Durchsuchbar
+ field_default_value: Standardwert
+ field_comments_sorting: Kommentare anzeigen
+ field_parent_title: Übergeordnete Seite
+
+ setting_app_title: Applikations-Titel
+ setting_app_subtitle: Applikations-Untertitel
+ setting_welcome_text: Willkommenstext
+ setting_default_language: Default-Sprache
+ setting_login_required: Authentisierung erforderlich
+ setting_self_registration: Anmeldung ermöglicht
+ setting_attachment_max_size: Max. Dateigröße
+ setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export
+ setting_mail_from: E-Mail-Absender
+ setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden
+ setting_plain_text_mail: Nur reinen Text (kein HTML) senden
+ setting_host_name: Hostname
+ setting_text_formatting: Textformatierung
+ setting_wiki_compression: Wiki-Historie komprimieren
+ setting_feeds_limit: Max. Anzahl Einträge pro Atom-Feed
+ setting_default_projects_public: Neue Projekte sind standardmäßig öffentlich
+ setting_autofetch_changesets: Changesets automatisch abrufen
+ setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen
+ setting_commit_ref_keywords: Schlüsselwörter (Beziehungen)
+ setting_commit_fix_keywords: Schlüsselwörter (Status)
+ setting_autologin: Automatische Anmeldung
+ setting_date_format: Datumsformat
+ setting_time_format: Zeitformat
+ setting_cross_project_issue_relations: Ticket-Beziehungen zwischen Projekten erlauben
+ setting_issue_list_default_columns: Default-Spalten in der Ticket-Auflistung
+ setting_repositories_encodings: Kodierungen der Projektarchive
+ setting_commit_logs_encoding: Kodierung der Commit-Log-Meldungen
+ setting_emails_footer: E-Mail-Fußzeile
+ setting_protocol: Protokoll
+ setting_per_page_options: Objekte pro Seite
+ setting_user_format: Benutzer-Anzeigeformat
+ setting_activity_days_default: Anzahl Tage pro Seite der Projekt-Aktivität
+ setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen
+ setting_enabled_scm: Aktivierte Versionskontrollsysteme
+ setting_mail_handler_api_enabled: Abruf eingehender E-Mails aktivieren
+ setting_mail_handler_api_key: API-Schlüssel
+ setting_sequential_project_identifiers: Fortlaufende Projektkennungen generieren
+ setting_gravatar_enabled: Gravatar Benutzerbilder benutzen
+ setting_diff_max_lines_displayed: Maximale Anzahl anzuzeigender Diff-Zeilen
+
+ permission_edit_project: Projekt bearbeiten
+ permission_select_project_modules: Projektmodule auswählen
+ permission_manage_members: Mitglieder verwalten
+ permission_manage_versions: Versionen verwalten
+ permission_manage_categories: Ticket-Kategorien verwalten
+ permission_add_issues: Tickets hinzufügen
+ permission_edit_issues: Tickets bearbeiten
+ permission_manage_issue_relations: Ticket-Beziehungen verwalten
+ permission_add_issue_notes: Kommentare hinzufügen
+ permission_edit_issue_notes: Kommentare bearbeiten
+ permission_edit_own_issue_notes: Eigene Kommentare bearbeiten
+ permission_move_issues: Tickets verschieben
+ permission_delete_issues: Tickets löschen
+ permission_manage_public_queries: Öffentliche Filter verwalten
+ permission_save_queries: Filter speichern
+ permission_view_gantt: Gantt-Diagramm ansehen
+ permission_view_calendar: Kalender ansehen
+ permission_view_issue_watchers: Liste der Beobachter ansehen
+ permission_add_issue_watchers: Beobachter hinzufügen
+ permission_log_time: Aufwände buchen
+ permission_view_time_entries: Gebuchte Aufwände ansehen
+ permission_edit_time_entries: Gebuchte Aufwände bearbeiten
+ permission_edit_own_time_entries: Selbst gebuchte Aufwände bearbeiten
+ permission_manage_news: News verwalten
+ permission_comment_news: News kommentieren
+ permission_manage_documents: Dokumente verwalten
+ permission_view_documents: Dokumente ansehen
+ permission_manage_files: Dateien verwalten
+ permission_view_files: Dateien ansehen
+ permission_manage_wiki: Wiki verwalten
+ permission_rename_wiki_pages: Wiki-Seiten umbenennen
+ permission_delete_wiki_pages: Wiki-Seiten löschen
+ permission_view_wiki_pages: Wiki ansehen
+ permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
+ permission_edit_wiki_pages: Wiki-Seiten bearbeiten
+ permission_delete_wiki_pages_attachments: Anhänge löschen
+ permission_protect_wiki_pages: Wiki-Seiten schützen
+ permission_manage_repository: Projektarchiv verwalten
+ permission_browse_repository: Projektarchiv ansehen
+ permission_view_changesets: Changesets ansehen
+ permission_commit_access: Commit-Zugriff (über WebDAV)
+ permission_manage_boards: Foren verwalten
+ permission_view_messages: Forenbeiträge ansehen
+ permission_add_messages: Forenbeiträge hinzufügen
+ permission_edit_messages: Forenbeiträge bearbeiten
+ permission_edit_own_messages: Eigene Forenbeiträge bearbeiten
+ permission_delete_messages: Forenbeiträge löschen
+ permission_delete_own_messages: Eigene Forenbeiträge löschen
+
+ project_module_issue_tracking: Ticket-Verfolgung
+ project_module_time_tracking: Zeiterfassung
+ project_module_news: News
+ project_module_documents: Dokumente
+ project_module_files: Dateien
+ project_module_wiki: Wiki
+ project_module_repository: Projektarchiv
+ project_module_boards: Foren
+
+ label_user: Benutzer
+ label_user_plural: Benutzer
+ label_user_new: Neuer Benutzer
+ label_project: Projekt
+ label_project_new: Neues Projekt
+ label_project_plural: Projekte
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Alle Projekte
+ label_project_latest: Neueste Projekte
+ label_issue: Ticket
+ label_issue_new: Neues Ticket
+ label_issue_plural: Tickets
+ label_issue_view_all: Alle Tickets anzeigen
+ label_issues_by: "Tickets von {{value}}"
+ label_issue_added: Ticket hinzugefügt
+ label_issue_updated: Ticket aktualisiert
+ label_document: Dokument
+ label_document_new: Neues Dokument
+ label_document_plural: Dokumente
+ label_document_added: Dokument hinzugefügt
+ label_role: Rolle
+ label_role_plural: Rollen
+ label_role_new: Neue Rolle
+ label_role_and_permissions: Rollen und Rechte
+ label_member: Mitglied
+ label_member_new: Neues Mitglied
+ label_member_plural: Mitglieder
+ label_tracker: Tracker
+ label_tracker_plural: Tracker
+ label_tracker_new: Neuer Tracker
+ label_workflow: Workflow
+ label_issue_status: Ticket-Status
+ label_issue_status_plural: Ticket-Status
+ label_issue_status_new: Neuer Status
+ label_issue_category: Ticket-Kategorie
+ label_issue_category_plural: Ticket-Kategorien
+ label_issue_category_new: Neue Kategorie
+ label_custom_field: Benutzerdefiniertes Feld
+ label_custom_field_plural: Benutzerdefinierte Felder
+ label_custom_field_new: Neues Feld
+ label_enumerations: Aufzählungen
+ label_enumeration_new: Neuer Wert
+ label_information: Information
+ label_information_plural: Informationen
+ label_please_login: Anmelden
+ label_register: Registrieren
+ label_password_lost: Kennwort vergessen
+ label_home: Hauptseite
+ label_my_page: Meine Seite
+ label_my_account: Mein Konto
+ label_my_projects: Meine Projekte
+ label_administration: Administration
+ label_login: Anmelden
+ label_logout: Abmelden
+ label_help: Hilfe
+ label_reported_issues: Gemeldete Tickets
+ label_assigned_to_me_issues: Mir zugewiesen
+ label_last_login: Letzte Anmeldung
+ label_registered_on: Angemeldet am
+ label_activity: Aktivität
+ label_overall_activity: Aktivität aller Projekte anzeigen
+ label_user_activity: "Aktivität von {{value}}"
+ label_new: Neu
+ label_logged_as: Angemeldet als
+ label_environment: Environment
+ label_authentication: Authentifizierung
+ label_auth_source: Authentifizierungs-Modus
+ label_auth_source_new: Neuer Authentifizierungs-Modus
+ label_auth_source_plural: Authentifizierungs-Arten
+ label_subproject_plural: Unterprojekte
+ label_and_its_subprojects: "{{value}} und dessen Unterprojekte"
+ label_min_max_length: Länge (Min. - Max.)
+ label_list: Liste
+ label_date: Datum
+ label_integer: Zahl
+ label_float: Fließkommazahl
+ label_boolean: Boolean
+ label_string: Text
+ label_text: Langer Text
+ label_attribute: Attribut
+ label_attribute_plural: Attribute
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Nichts anzuzeigen
+ label_change_status: Statuswechsel
+ label_history: Historie
+ label_attachment: Datei
+ label_attachment_new: Neue Datei
+ label_attachment_delete: Anhang löschen
+ label_attachment_plural: Dateien
+ label_file_added: Datei hinzugefügt
+ label_report: Bericht
+ label_report_plural: Berichte
+ label_news: News
+ label_news_new: News hinzufügen
+ label_news_plural: News
+ label_news_latest: Letzte News
+ label_news_view_all: Alle News anzeigen
+ label_news_added: News hinzugefügt
+ label_change_log: Change-Log
+ label_settings: Konfiguration
+ label_overview: Übersicht
+ label_version: Version
+ label_version_new: Neue Version
+ label_version_plural: Versionen
+ label_confirmation: Bestätigung
+ label_export_to: "Auch abrufbar als:"
+ label_read: Lesen...
+ label_public_projects: Öffentliche Projekte
+ label_open_issues: offen
+ label_open_issues_plural: offen
+ label_closed_issues: geschlossen
+ label_closed_issues_plural: geschlossen
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Gesamtzahl
+ label_permissions: Berechtigungen
+ label_current_status: Gegenwärtiger Status
+ label_new_statuses_allowed: Neue Berechtigungen
+ label_all: alle
+ label_none: kein
+ label_nobody: Niemand
+ label_next: Weiter
+ label_previous: Zurück
+ label_used_by: Benutzt von
+ label_details: Details
+ label_add_note: Kommentar hinzufügen
+ label_per_page: Pro Seite
+ label_calendar: Kalender
+ label_months_from: Monate ab
+ label_gantt: Gantt-Diagramm
+ label_internal: Intern
+ label_last_changes: "{{count}} letzte Änderungen"
+ label_change_view_all: Alle Änderungen anzeigen
+ label_personalize_page: Diese Seite anpassen
+ label_comment: Kommentar
+ label_comment_plural: Kommentare
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Kommentar hinzufügen
+ label_comment_added: Kommentar hinzugefügt
+ label_comment_delete: Kommentar löschen
+ label_query: Benutzerdefinierte Abfrage
+ label_query_plural: Benutzerdefinierte Berichte
+ label_query_new: Neuer Bericht
+ label_filter_add: Filter hinzufügen
+ label_filter_plural: Filter
+ label_equals: ist
+ label_not_equals: ist nicht
+ label_in_less_than: in weniger als
+ label_in_more_than: in mehr als
+ label_in: an
+ label_today: heute
+ label_all_time: gesamter Zeitraum
+ label_yesterday: gestern
+ label_this_week: aktuelle Woche
+ label_last_week: vorige Woche
+ label_last_n_days: "die letzten {{count}} Tage"
+ label_this_month: aktueller Monat
+ label_last_month: voriger Monat
+ label_this_year: aktuelles Jahr
+ label_date_range: Zeitraum
+ label_less_than_ago: vor weniger als
+ label_more_than_ago: vor mehr als
+ label_ago: vor
+ label_contains: enthält
+ label_not_contains: enthält nicht
+ label_day_plural: Tage
+ label_repository: Projektarchiv
+ label_repository_plural: Projektarchive
+ label_browse: Codebrowser
+ label_modification: "{{count}} Änderung"
+ label_modification_plural: "{{count}} Änderungen"
+ label_revision: Revision
+ label_revision_plural: Revisionen
+ label_associated_revisions: Zugehörige Revisionen
+ label_added: hinzugefügt
+ label_modified: geändert
+ label_copied: kopiert
+ label_renamed: umbenannt
+ label_deleted: gelöscht
+ label_latest_revision: Aktuellste Revision
+ label_latest_revision_plural: Aktuellste Revisionen
+ label_view_revisions: Revisionen anzeigen
+ label_max_size: Maximale Größe
+ label_sort_highest: An den Anfang
+ label_sort_higher: Eins höher
+ label_sort_lower: Eins tiefer
+ label_sort_lowest: Ans Ende
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Fällig in {{value}}"
+ label_roadmap_overdue: "{{value}} verspätet"
+ label_roadmap_no_issues: Keine Tickets für diese Version
+ label_search: Suche
+ label_result_plural: Resultate
+ label_all_words: Alle Wörter
+ label_wiki: Wiki
+ label_wiki_edit: Wiki-Bearbeitung
+ label_wiki_edit_plural: Wiki-Bearbeitungen
+ label_wiki_page: Wiki-Seite
+ label_wiki_page_plural: Wiki-Seiten
+ label_index_by_title: Seiten nach Titel sortiert
+ label_index_by_date: Seiten nach Datum sortiert
+ label_current_version: Gegenwärtige Version
+ label_preview: Vorschau
+ label_feed_plural: Feeds
+ label_changes_details: Details aller Änderungen
+ label_issue_tracking: Tickets
+ label_spent_time: Aufgewendete Zeit
+ label_f_hour: "{{value}} Stunde"
+ label_f_hour_plural: "{{value}} Stunden"
+ label_time_tracking: Zeiterfassung
+ label_change_plural: Änderungen
+ label_statistics: Statistiken
+ label_commits_per_month: Übertragungen pro Monat
+ label_commits_per_author: Übertragungen pro Autor
+ label_view_diff: Unterschiede anzeigen
+ label_diff_inline: inline
+ label_diff_side_by_side: nebeneinander
+ label_options: Optionen
+ label_copy_workflow_from: Workflow kopieren von
+ label_permissions_report: Berechtigungsübersicht
+ label_watched_issues: Beobachtete Tickets
+ label_related_issues: Zugehörige Tickets
+ label_applied_status: Zugewiesener Status
+ label_loading: Lade...
+ label_relation_new: Neue Beziehung
+ label_relation_delete: Beziehung löschen
+ label_relates_to: Beziehung mit
+ label_duplicates: Duplikat von
+ label_duplicated_by: Dupliziert durch
+ label_blocks: Blockiert
+ label_blocked_by: Blockiert durch
+ label_precedes: Vorgänger von
+ label_follows: folgt
+ label_end_to_start: Ende - Anfang
+ label_end_to_end: Ende - Ende
+ label_start_to_start: Anfang - Anfang
+ label_start_to_end: Anfang - Ende
+ label_stay_logged_in: Angemeldet bleiben
+ label_disabled: gesperrt
+ label_show_completed_versions: Abgeschlossene Versionen anzeigen
+ label_me: ich
+ label_board: Forum
+ label_board_new: Neues Forum
+ label_board_plural: Foren
+ label_topic_plural: Themen
+ label_message_plural: Forenbeiträge
+ label_message_last: Letzter Forenbeitrag
+ label_message_new: Neues Thema
+ label_message_posted: Forenbeitrag hinzugefügt
+ label_reply_plural: Antworten
+ label_send_information: Sende Kontoinformationen zum Benutzer
+ label_year: Jahr
+ label_month: Monat
+ label_week: Woche
+ label_date_from: Von
+ label_date_to: Bis
+ label_language_based: Sprachabhängig
+ label_sort_by: "Sortiert nach {{value}}"
+ label_send_test_email: Test-E-Mail senden
+ label_feeds_access_key_created_on: "Atom-Zugriffsschlüssel vor {{value}} erstellt"
+ label_module_plural: Module
+ label_added_time_by: "Von {{author}} vor {{age}} hinzugefügt"
+ label_updated_time_by: "Von {{author}} vor {{age}} aktualisiert"
+ label_updated_time: "Vor {{value}} aktualisiert"
+ label_jump_to_a_project: Zu einem Projekt springen...
+ label_file_plural: Dateien
+ label_changeset_plural: Changesets
+ label_default_columns: Default-Spalten
+ label_no_change_option: (Keine Änderung)
+ label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten
+ label_theme: Stil
+ label_default: Default
+ label_search_titles_only: Nur Titel durchsuchen
+ label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten"
+ label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten..."
+ label_user_mail_option_none: "Nur für Dinge, die ich beobachte oder an denen ich beteiligt bin"
+ label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe."
+ label_registration_activation_by_email: Kontoaktivierung durch E-Mail
+ label_registration_manual_activation: Manuelle Kontoaktivierung
+ label_registration_automatic_activation: Automatische Kontoaktivierung
+ label_display_per_page: "Pro Seite: {{value}}"
+ label_age: Geändert vor
+ label_change_properties: Eigenschaften ändern
+ label_general: Allgemein
+ label_more: Mehr
+ label_scm: Versionskontrollsystem
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP-Authentifizierung
+ label_downloads_abbr: D/L
+ label_optional_description: Beschreibung (optional)
+ label_add_another_file: Eine weitere Datei hinzufügen
+ label_preferences: Präferenzen
+ label_chronological_order: in zeitlicher Reihenfolge
+ label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge
+ label_planning: Terminplanung
+ label_incoming_emails: Eingehende E-Mails
+ label_generate_key: Generieren
+ label_issue_watchers: Beobachter
+ label_example: Beispiel
+
+ button_login: Anmelden
+ button_submit: OK
+ button_save: Speichern
+ button_check_all: Alles auswählen
+ button_uncheck_all: Alles abwählen
+ button_delete: Löschen
+ button_create: Anlegen
+ button_test: Testen
+ button_edit: Bearbeiten
+ button_add: Hinzufügen
+ button_change: Wechseln
+ button_apply: Anwenden
+ button_clear: Zurücksetzen
+ button_lock: Sperren
+ button_unlock: Entsperren
+ button_download: Download
+ button_list: Liste
+ button_view: Anzeigen
+ button_move: Verschieben
+ button_back: Zurück
+ button_cancel: Abbrechen
+ button_activate: Aktivieren
+ button_sort: Sortieren
+ button_log_time: Aufwand buchen
+ button_rollback: Auf diese Version zurücksetzen
+ button_watch: Beobachten
+ button_unwatch: Nicht beobachten
+ button_reply: Antworten
+ button_archive: Archivieren
+ button_unarchive: Entarchivieren
+ button_reset: Zurücksetzen
+ button_rename: Umbenennen
+ button_change_password: Kennwort ändern
+ button_copy: Kopieren
+ button_annotate: Annotieren
+ button_update: Aktualisieren
+ button_configure: Konfigurieren
+ button_quote: Zitieren
+
+ status_active: aktiv
+ status_registered: angemeldet
+ status_locked: gesperrt
+
+ text_select_mail_notifications: Bitte wählen Sie die Aktionen aus, für die eine Mailbenachrichtigung gesendet werden soll
+ text_regexp_info: z. B. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 heißt keine Beschränkung
+ text_project_destroy_confirmation: Sind Sie sicher, dass sie das Projekt löschen wollen?
+ text_subprojects_destroy_warning: "Dessen Unterprojekte ({{value}}) werden ebenfalls gelöscht."
+ text_workflow_edit: Workflow zum Bearbeiten auswählen
+ text_are_you_sure: Sind Sie sicher?
+ text_tip_task_begin_day: Aufgabe, die an diesem Tag beginnt
+ text_tip_task_end_day: Aufgabe, die an diesem Tag endet
+ text_tip_task_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet
+ text_project_identifier_info: 'Kleinbuchstaben (a-z), Ziffern und Bindestriche erlaubt.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden.'
+ text_caracters_maximum: "Max. {{count}} Zeichen."
+ text_caracters_minimum: "Muss mindestens {{count}} Zeichen lang sein."
+ text_length_between: "Länge zwischen {{min}} und {{max}} Zeichen."
+ text_tracker_no_workflow: Kein Workflow für diesen Tracker definiert.
+ text_unallowed_characters: Nicht erlaubte Zeichen
+ text_comma_separated: Mehrere Werte erlaubt (durch Komma getrennt).
+ text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log-Meldungen
+ text_issue_added: "Ticket {{id}} wurde erstellt by {{author}}."
+ text_issue_updated: "Ticket {{id}} wurde aktualisiert by {{author}}."
+ text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtlichem Inhalt löschen möchten?
+ text_issue_category_destroy_question: "Einige Tickets ({{count}}) sind dieser Kategorie zugeodnet. Was möchten Sie tun?"
+ text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen
+ text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen
+ text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z. B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)."
+ text_no_configuration_data: "Rollen, Tracker, Ticket-Status und Workflows wurden noch nicht konfiguriert.\nEs ist sehr zu empfehlen, die Standard-Konfiguration zu laden. Sobald sie geladen ist, können Sie sie abändern."
+ text_load_default_configuration: Standard-Konfiguration laden
+ text_status_changed_by_changeset: "Status geändert durch Changeset {{value}}."
+ text_issues_destroy_confirmation: 'Sind Sie sicher, dass Sie die ausgewählten Tickets löschen möchten?'
+ text_select_project_modules: 'Bitte wählen Sie die Module aus, die in diesem Projekt aktiviert sein sollen:'
+ text_default_administrator_account_changed: Administrator-Kennwort geändert
+ text_file_repository_writable: Verzeichnis für Dateien beschreibbar
+ text_plugin_assets_writable: Verzeichnis für Plugin-Assets beschreibbar
+ text_rmagick_available: RMagick verfügbar (optional)
+ text_destroy_time_entries_question: Es wurden bereits {{hours}} Stunden auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?
+ text_destroy_time_entries: Gebuchte Aufwände löschen
+ text_assign_time_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen
+ text_reassign_time_entries: 'Gebuchte Aufwände diesem Ticket zuweisen:'
+ text_user_wrote: "{{value}} schrieb:"
+ text_enumeration_destroy_question: "{{count}} Objekte sind diesem Wert zugeordnet."
+ text_enumeration_category_reassign_to: 'Die Objekte stattdessen diesem Wert zuordnen:'
+ text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/email.yml vor und starten Sie die Applikation neu."
+ text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der Redmine-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen Redmine- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet."
+ text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.'
+
+ default_role_manager: Manager
+ default_role_developper: Entwickler
+ default_role_reporter: Reporter
+ default_tracker_bug: Fehler
+ default_tracker_feature: Feature
+ default_tracker_support: Unterstützung
+ default_issue_status_new: Neu
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Gelöst
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Erledigt
+ default_issue_status_rejected: Abgewiesen
+ default_doc_category_user: Benutzerdokumentation
+ default_doc_category_tech: Technische Dokumentation
+ default_priority_low: Niedrig
+ default_priority_normal: Normal
+ default_priority_high: Hoch
+ default_priority_urgent: Dringend
+ default_priority_immediate: Sofort
+ default_activity_design: Design
+ default_activity_development: Entwicklung
+
+ enumeration_issue_priorities: Ticket-Prioritäten
+ enumeration_doc_categories: Dokumentenkategorien
+ enumeration_activities: Aktivitäten (Zeiterfassung)
+ field_editable: Editable
+ label_display: Display
+ button_create_and_continue: Anlegen und weiter
+ text_custom_field_possible_values_info: 'Eine Zeile pro Wert'
+ setting_repository_log_display_limit: Maximale Anzahl angezeigter Revisionen des Datei Logs
+ setting_file_max_size_displayed: Maximale Größe der abgezeigten Textdatei
+ field_watcher: Beobachter
+ setting_openid: Erlaube OpenID Anmeldung und Registrierung
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: oder anmeldung mit OpenID
+ field_content: Inhalt
+ label_descending: Absteigend
+ label_sort: Sortierung
+ label_ascending: Aufsteigend
+ label_date_from_to: Von {{start}} bis {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: "<="
+ text_wiki_page_destroy_question: Diese Seite hat {{descendants}} Unterseite(n). Sind Sie sicher?
+ text_wiki_page_reassign_children: Ordne Unterseite dieser Seite zu
+ text_wiki_page_nullify_children: Behalte Unterseite als Hauptseite
+ text_wiki_page_destroy_children: Lösche alle Unterseiten
+ setting_password_min_length: Mindestlänge des Passworts
+ field_group_by: Gruppiere Ergebnisse nach
+ mail_subject_wiki_content_updated: "Wiki-Seite '{{page}}' aktualisiert"
+ label_wiki_content_added: Wiki-Seite hinzugefügt
+ mail_subject_wiki_content_added: "Wiki-Seite '{{page}}' hinzugefügt"
+ mail_body_wiki_content_added: "Die Wiki-Seite '{{page}}' wurde von {{author}} hinzugefügt."
+ label_wiki_content_updated: Wiki-Seite aktualisiert.
+ mail_body_wiki_content_updated: "Die Wiki-Seite '{{page}}' wurde von {{author}} aktualisiert."
+ permission_add_project: Erstelle Projekt
+ setting_new_project_user_role_id: Rolle einem Nicht-Administrator zugeordnet, welcher ein Projekt erstellt
+ 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
--- /dev/null
+# Greek translations for Ruby on Rails
+# by Vaggelis Typaldos (vtypal@gmail.com), Spyros Raptis (spirosrap@gmail.com)
+
+el:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%m/%d/%Y"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Κυριακή, Δευτέρα, Τρίτη, Τετάρτη, Πέμπτη, Παρασκευή, Σάββατο]
+ 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: "%m/%d/%Y %I:%M %p"
+ time: "%I:%M %p"
+ short: "%d %b %H:%M"
+ long: "%B %d, %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: "λιγότερο από ένα λεπτό"
+ other: "λιγότερο από {{count}} λεπτά"
+ x_minutes:
+ one: "1 λεπτό"
+ other: "{{count}} λεπτά"
+ about_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}} χρόνια"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ 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: "Αυτή η σχέση θα δημιουργήσει κυκλικές εξαρτήσεις"
+
+ actionview_instancetag_blank_option: Παρακαλώ επιλέξτε
+
+ general_text_No: 'Όχι'
+ general_text_Yes: 'Ναι'
+ general_text_no: 'όχι'
+ general_text_yes: 'ναι'
+ general_lang_name: 'Ελληνικά'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '7'
+
+ notice_account_updated: Ο λογαριασμός ενημερώθηκε επιτυχώς.
+ notice_account_invalid_creditentials: Άκυρο όνομα χρήστη ή κωδικού πρόσβασης
+ notice_account_password_updated: Ο κωδικός πρόσβασης ενημερώθηκε επιτυχώς.
+ notice_account_wrong_password: Λάθος κωδικός πρόσβασης
+ notice_account_register_done: Ο λογαριασμός δημιουργήθηκε επιτυχώς. Για να ενεργοποιήσετε το λογαριασμό σας, πατήστε το σύνδεσμο που σας έχει αποσταλεί με email.
+ notice_account_unknown_email: Άγνωστος χρήστης.
+ notice_can_t_change_password: Αυτός ο λογαριασμός χρησιμοποιεί εξωτερική πηγή πιστοποίησης. Δεν είναι δυνατόν να αλλάξετε τον κωδικό πρόσβασης.
+ notice_account_lost_email_sent: Σας έχει αποσταλεί email με οδηγίες για την επιλογή νέου κωδικού πρόσβασης.
+ notice_account_activated: Ο λογαριασμός σας έχει ενεργοποιηθεί. Τώρα μπορείτε να συνδεθείτε.
+ notice_successful_create: Επιτυχής δημιουργία.
+ notice_successful_update: Επιτυχής ενημέρωση.
+ notice_successful_delete: Επιτυχής διαγραφή.
+ notice_successful_connection: Επιτυχής σύνδεση.
+ notice_file_not_found: Η σελίδα που ζητήσατε δεν υπάρχει ή έχει αφαιρεθεί.
+ notice_locking_conflict: Τα δεδομένα έχουν ενημερωθεί από άλλο χρήστη.
+ notice_not_authorized: Δεν έχετε δικαίωμα πρόσβασης σε αυτή τη σελίδα.
+ notice_email_sent: "Ένα μήνυμα ηλεκτρονικού ταχυδρομείου εστάλη στο {{value}}"
+ notice_email_error: "Σφάλμα κατά την αποστολή του μηνύματος στο ({{value}})"
+ notice_feeds_access_key_reseted: Έγινε επαναφορά στο κλειδί πρόσβασης RSS.
+ notice_failed_to_save_issues: "Αποτυχία αποθήκευσης {{count}} θεμα(των) από τα {{total}} επιλεγμένα: {{ids}}."
+ notice_no_issue_selected: "Κανένα θέμα δεν είναι επιλεγμένο! Παρακαλούμε, ελέγξτε τα θέματα που θέλετε να επεξεργαστείτε."
+ notice_account_pending: "Ο λογαριασμός σας έχει δημιουργηθεί και είναι σε στάδιο έγκρισης από τον διαχειριστή."
+ notice_default_data_loaded: Οι προεπιλεγμένες ρυθμίσεις φορτώθηκαν επιτυχώς.
+ notice_unable_delete_version: Αδύνατον να διαγραφεί η έκδοση.
+
+ error_can_t_load_default_data: "Οι προεπιλεγμένες ρυθμίσεις δεν μπόρεσαν να φορτωθούν:: {{value}}"
+ error_scm_not_found: "Η εγγραφή ή η αναθεώρηση δεν βρέθηκε στο αποθετήριο."
+ error_scm_command_failed: "Παρουσιάστηκε σφάλμα κατά την προσπάθεια πρόσβασης στο αποθετήριο: {{value}}"
+ error_scm_annotate: "Η καταχώριση δεν υπάρχει ή δεν μπορεί να σχολιαστεί."
+ error_issue_not_found_in_project: 'Το θέμα δεν βρέθηκε ή δεν ανήκει σε αυτό το έργο'
+ error_no_tracker_in_project: 'Δεν υπάρχει ανιχνευτής για αυτό το έργο. Παρακαλώ ελέγξτε τις ρυθμίσεις του έργου.'
+ error_no_default_issue_status: 'Δεν έχει οριστεί η προεπιλογή κατάστασης θεμάτων. Παρακαλώ ελέγξτε τις ρυθμίσεις σας (Μεταβείτε στην "Διαχείριση -> Κατάσταση θεμάτων").'
+
+ 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}} θέμα(τα) με προθεσμία στις επόμενες ημέρες"
+ mail_body_reminder: "{{count}}θέμα(τα) που έχουν ανατεθεί σε σας, με προθεσμία στις επόμενες {{days}} ημέρες:"
+ mail_subject_wiki_content_added: "'προστέθηκε η σελίδα wiki {{page}}' "
+ mail_body_wiki_content_added: "Η σελίδα wiki '{{page}}' προστέθηκε από τον {{author}}."
+ mail_subject_wiki_content_updated: "'ενημερώθηκε η σελίδα wiki {{page}}' "
+ mail_body_wiki_content_updated: "Η σελίδα wiki '{{page}}' ενημερώθηκε από τον {{author}}."
+
+ gui_validation_error: 1 σφάλμα
+ gui_validation_error_plural: "{{count}} σφάλματα"
+
+ field_name: Όνομα
+ field_description: Περιγραφή
+ field_summary: Συνοπτικά
+ field_is_required: Απαιτείται
+ field_firstname: Όνομα
+ field_lastname: Επώνυμο
+ field_mail: Email
+ field_filename: Αρχείο
+ field_filesize: Μέγεθος
+ field_downloads: Μεταφορτώσεις
+ field_author: Συγγραφέας
+ field_created_on: Δημιουργήθηκε
+ field_updated_on: Ενημερώθηκε
+ field_field_format: Μορφοποίηση
+ field_is_for_all: Για όλα τα έργα
+ field_possible_values: Πιθανές τιμές
+ field_regexp: Κανονική παράσταση
+ field_min_length: Ελάχιστο μήκος
+ field_max_length: Μέγιστο μήκος
+ field_value: Τιμή
+ field_category: Κατηγορία
+ field_title: Τίτλος
+ field_project: Έργο
+ field_issue: Θέμα
+ field_status: Κατάσταση
+ field_notes: Σημειώσεις
+ field_is_closed: Κλειστά θέματα
+ field_is_default: Προεπιλεγμένη τιμή
+ field_tracker: Ανιχνευτής
+ field_subject: Θέμα
+ field_due_date: Προθεσμία
+ field_assigned_to: Ανάθεση σε
+ field_priority: Προτεραιότητα
+ field_fixed_version: Στόχος έκδοσης
+ field_user: Χρήστης
+ field_role: Ρόλος
+ field_homepage: Αρχική σελίδα
+ field_is_public: Δημόσιο
+ field_parent: Επιμέρους έργο του
+ field_is_in_chlog: Προβολή θεμάτων στο ιστορικό αλλαγών
+ field_is_in_roadmap: Προβολή θεμάτων στο χάρτη πορείας
+ field_login: Όνομα χρήστη
+ field_mail_notification: Ειδοποιήσεις email
+ 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: Ιδιότητα email
+ field_onthefly: Άμεση δημιουργία χρήστη
+ field_start_date: Εκκίνηση
+ field_done_ratio: % επιτεύχθη
+ field_auth_source: Τρόπος πιστοποίησης
+ field_hide_mail: Απόκρυψη διεύθυνσης email
+ 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: Ομαδικά αποτελέσματα από
+
+ 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: Μετάδοση διεύθυνσης email
+ setting_bcc_recipients: Αποδέκτες κρυφής κοινοποίησης (bcc)
+ setting_plain_text_mail: Email απλού κειμένου (όχι HTML)
+ setting_host_name: Όνομα κόμβου και διαδρομή
+ setting_text_formatting: Μορφοποίηση κειμένου
+ setting_wiki_compression: Συμπίεση ιστορικού wiki
+ setting_feeds_limit: Feed περιορισμού περιεχομένου
+ setting_default_projects_public: Τα νέα έργα έχουν προεπιλεγεί ως δημόσια
+ setting_autofetch_changesets: Αυτόματη λήψη commits
+ setting_sys_api_enabled: Ενεργοποίηση WS για διαχείριση αποθετηρίου
+ setting_commit_ref_keywords: Αναφορά σε λέξεις-κλειδιά
+ setting_commit_fix_keywords: Καθορισμός σε λέξεις-κλειδιά
+ setting_autologin: Αυτόματη σύνδεση
+ setting_date_format: Μορφή ημερομηνίας
+ setting_time_format: Μορφή ώρας
+ setting_cross_project_issue_relations: Επιτρέψτε συσχετισμό θεμάτων σε διασταύρωση-έργων
+ setting_issue_list_default_columns: Προκαθορισμένες εμφανιζόμενες στήλες στη λίστα θεμάτων
+ setting_repositories_encodings: Κωδικοποίηση χαρακτήρων αποθετηρίου
+ setting_commit_logs_encoding: Κωδικοποίηση μηνυμάτων commit
+ setting_emails_footer: Υποσέλιδο στα email
+ setting_protocol: Πρωτόκολο
+ setting_per_page_options: Αντικείμενα ανά σελίδα επιλογών
+ setting_user_format: Μορφή εμφάνισης χρηστών
+ setting_activity_days_default: Ημέρες που εμφανίζεται στη δραστηριότητα έργου
+ setting_display_subprojects_issues: Εμφάνιση από προεπιλογή θεμάτων επιμέρους έργων στα κύρια έργα
+ setting_enabled_scm: Ενεργοποίηση SCM
+ setting_mail_handler_api_enabled: Ενεργοποίηση WS για εισερχόμενα email
+ setting_mail_handler_api_key: κλειδί API
+ setting_sequential_project_identifiers: Δημιουργία διαδοχικών αναγνωριστικών έργου
+ setting_gravatar_enabled: Χρήση Gravatar εικονιδίων χρηστών
+ setting_diff_max_lines_displayed: Μεγ.αριθμός εμφάνισης γραμμών diff
+ setting_file_max_size_displayed: Μεγ.μέγεθος των αρχείων απλού κειμένου που εμφανίζονται σε σειρά
+ setting_repository_log_display_limit: Μέγιστος αριθμός αναθεωρήσεων που εμφανίζονται στο ιστορικό αρχείου
+ setting_openid: Επιτρέψτε συνδέσεις OpenID και εγγραφή
+ setting_password_min_length: Ελάχιστο μήκος κωδικού πρόσβασης
+ setting_new_project_user_role_id: Απόδοση ρόλου σε χρήστη μη-διαχειριστή όταν δημιουργεί ένα έργο
+
+ permission_add_project: Δημιουργία έργου
+ permission_edit_project: Επεξεργασία έργου
+ permission_select_project_modules: Επιλογή μονάδων έργου
+ permission_manage_members: Διαχείριση μελών
+ permission_manage_versions: Διαχείριση εκδόσεων
+ permission_manage_categories: Διαχείριση κατηγοριών θεμάτων
+ permission_add_issues: Προσθήκη θεμάτων
+ permission_edit_issues: Επεξεργασία θεμάτων
+ permission_manage_issue_relations: Διαχείριση συσχετισμών θεμάτων
+ permission_add_issue_notes: Προσθήκη σημειώσεων
+ permission_edit_issue_notes: Επεξεργασία σημειώσεων
+ permission_edit_own_issue_notes: Επεξεργασία δικών μου σημειώσεων
+ permission_move_issues: Μεταφορά θεμάτων
+ permission_delete_issues: Διαγραφή θεμάτων
+ permission_manage_public_queries: Διαχείριση δημόσιων αναζητήσεων
+ permission_save_queries: Αποθήκευση αναζητήσεων
+ permission_view_gantt: Προβολή διαγράμματος gantt
+ permission_view_calendar: Προβολή ημερολογίου
+ permission_view_issue_watchers: Προβολή λίστας παρατηρητών
+ permission_add_issue_watchers: Προσθήκη παρατηρητών
+ permission_log_time: Ιστορικό χρόνου που δαπανήθηκε
+ permission_view_time_entries: Προβολή χρόνου που δαπανήθηκε
+ permission_edit_time_entries: Επεξεργασία ιστορικού χρόνου
+ permission_edit_own_time_entries: Επεξεργασία δικού μου ιστορικού χρόνου
+ permission_manage_news: Διαχείριση νέων
+ permission_comment_news: Σχολιασμός νέων
+ permission_manage_documents: Διαχείριση εγγράφων
+ permission_view_documents: Προβολή εγγράφων
+ permission_manage_files: Διαχείριση αρχείων
+ permission_view_files: Προβολή αρχείων
+ permission_manage_wiki: Διαχείριση 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: Προβολή changesets
+ permission_commit_access: Πρόσβαση commit
+ permission_manage_boards: Διαχείριση πινάκων συζητήσεων
+ permission_view_messages: Προβολή μηνυμάτων
+ permission_add_messages: Αποστολή μηνυμάτων
+ permission_edit_messages: Επεξεργασία μηνυμάτων
+ permission_edit_own_messages: Επεξεργασία δικών μου μηνυμάτων
+ permission_delete_messages: Διαγραφή μηνυμάτων
+ permission_delete_own_messages: Διαγραφή δικών μου μηνυμάτων
+
+ project_module_issue_tracking: Ανίχνευση θεμάτων
+ project_module_time_tracking: Ανίχνευση χρόνου
+ project_module_news: Νέα
+ project_module_documents: Έγγραφα
+ project_module_files: Αρχεία
+ project_module_wiki: Wiki
+ project_module_repository: Αποθετήριο
+ project_module_boards: Πίνακες συζητήσεων
+
+ label_user: Χρήστης
+ label_user_plural: Χρήστες
+ label_user_new: Νέος Χρήστης
+ label_project: Έργο
+ label_project_new: Νέο έργο
+ label_project_plural: Έργα
+ label_x_projects:
+ zero: κανένα έργο
+ one: 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_administration: Διαχείριση
+ label_login: Σύνδεση
+ label_logout: Αποσύνδεση
+ label_help: Βοήθεια
+ label_reported_issues: Εισηγμένα θέματα
+ label_assigned_to_me_issues: Θέματα που έχουν ανατεθεί σε μένα
+ label_last_login: Τελευταία σύνδεση
+ label_registered_on: Εγγράφηκε την
+ label_activity: Δραστηριότητα
+ label_overall_activity: Συνολική δραστηριότητα
+ label_user_activity: "δραστηριότητα του {{value}}"
+ label_new: Νέο
+ label_logged_as: Σύνδεδεμένος ως
+ label_environment: Περιβάλλον
+ label_authentication: Πιστοποίηση
+ label_auth_source: Τρόπος πιστοποίησης
+ label_auth_source_new: Νέος τρόπος πιστοποίησης
+ label_auth_source_plural: Τρόποι πιστοποίησης
+ label_subproject_plural: Επιμέρους έργα
+ label_and_its_subprojects: "{{value}} και τα επιμέρους έργα του"
+ label_min_max_length: Ελάχ. - Μέγ. μήκος
+ label_list: Λίστα
+ label_date: Ημερομηνία
+ label_integer: Ακέραιος
+ label_float: Αριθμός κινητής υποδιαστολής
+ label_boolean: Λογικός
+ label_string: Κείμενο
+ label_text: Μακροσκελές κείμενο
+ label_attribute: Ιδιότητα
+ label_attribute_plural: Ιδιότητες
+ label_download: "{{count}} Μεταφόρτωση"
+ label_download_plural: "{{count}} Μεταφορτώσεις"
+ label_no_data: Δεν υπάρχουν δεδομένα
+ label_change_status: Αλλαγή κατάστασης
+ label_history: Ιστορικό
+ label_attachment: Αρχείο
+ label_attachment_new: Νέο αρχείο
+ label_attachment_delete: Διαγραφή αρχείου
+ label_attachment_plural: Αρχεία
+ label_file_added: Το αρχείο προστέθηκε
+ label_report: Αναφορά
+ label_report_plural: Αναφορές
+ label_news: Νέα
+ label_news_new: Προσθήκη νέων
+ label_news_plural: Νέα
+ label_news_latest: Τελευταία νέα
+ label_news_view_all: Προβολή όλων των νέων
+ label_news_added: Τα νέα προστέθηκαν
+ label_change_log: Αλλαγή ιστορικού
+ label_settings: Ρυθμίσεις
+ label_overview: Επισκόπηση
+ label_version: Έκδοση
+ label_version_new: Νέα έκδοση
+ label_version_plural: Εκδόσεις
+ label_confirmation: Επιβεβαίωση
+ label_export_to: 'Επίσης διαθέσιμο σε:'
+ label_read: Διάβασε...
+ label_public_projects: Δημόσια έργα
+ label_open_issues: Ανοικτό
+ label_open_issues_plural: Ανοικτά
+ label_closed_issues: Κλειστό
+ label_closed_issues_plural: Κλειστά
+ label_x_open_issues_abbr_on_total:
+ zero: 0 ανοικτά / {{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: 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_modification: "{{count}} τροποποίηση"
+ label_modification_plural: "{{count}} τροποποιήσεις"
+ label_branch: Branch
+ label_tag: Tag
+ label_revision: Αναθεώρηση
+ label_revision_plural: Αναθεωρήσεις
+ label_associated_revisions: Συνεταιρικές αναθεωρήσεις
+ label_added: προστέθηκε
+ label_modified: τροποποιήθηκε
+ label_copied: αντιγράφηκε
+ label_renamed: μετονομάστηκε
+ label_deleted: διαγράφηκε
+ label_latest_revision: Τελευταία αναθεώριση
+ label_latest_revision_plural: Τελευταίες αναθεωρήσεις
+ label_view_revisions: Προβολή αναθεωρήσεων
+ label_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_f_hour: "{{value}} ώρα"
+ label_f_hour_plural: "{{value}} ώρες"
+ label_time_tracking: Ανίχνευση χρόνου
+ label_change_plural: Αλλαγές
+ label_statistics: Στατιστικά
+ label_commits_per_month: Commits ανά μήνα
+ label_commits_per_author: Commits ανά συγγραφέα
+ label_view_diff: Προβολή διαφορών
+ label_diff_inline: σε σειρά
+ label_diff_side_by_side: αντικρυστά
+ label_options: Επιλογές
+ label_copy_workflow_from: Αντιγραφή ροής εργασίας από
+ label_permissions_report: Συνοπτικός πίνακας αδειών
+ label_watched_issues: Θέματα υπό παρακολούθηση
+ label_related_issues: Σχετικά θέματα
+ label_applied_status: Εφαρμογή κατάστασης
+ label_loading: Φορτώνεται...
+ label_relation_new: Νέα συσχέτιση
+ label_relation_delete: Διαγραφή συσχέτισης
+ label_relates_to: σχετικό με
+ label_duplicates: αντίγραφα
+ label_duplicated_by: αντιγράφηκε από
+ label_blocks: φραγές
+ label_blocked_by: φραγή από τον
+ label_precedes: προηγείται
+ label_follows: ακολουθεί
+ label_end_to_start: από το τέλος στην αρχή
+ label_end_to_end: από το τέλος στο τέλος
+ label_start_to_start: από την αρχή στην αρχή
+ label_start_to_end: από την αρχή στο τέλος
+ label_stay_logged_in: Παραμονή σύνδεσης
+ label_disabled: απενεργοποιημένη
+ label_show_completed_versions: Προβολή ολοκληρωμένων εκδόσεων
+ label_me: εγώ
+ label_board: Φόρουμ
+ label_board_new: Νέο φόρουμ
+ label_board_plural: Φόρουμ
+ label_topic_plural: Θέματα
+ label_message_plural: Μηνύματα
+ label_message_last: Τελευταίο μήνυμα
+ label_message_new: Νέο μήνυμα
+ label_message_posted: Το μήνυμα προστέθηκε
+ label_reply_plural: Απαντήσεις
+ label_send_information: Αποστολή πληροφοριών λογαριασμού στο χρήστη
+ label_year: Έτος
+ label_month: Μήνας
+ label_week: Εβδομάδα
+ label_date_from: Από
+ label_date_to: Έως
+ label_language_based: Με βάση τη γλώσσα του χρήστη
+ label_sort_by: "Ταξινόμηση ανά {{value}}"
+ label_send_test_email: Αποστολή δοκιμαστικού email
+ 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: Changesets
+ 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_no_self_notified: "Δεν θέλω να ειδοποιούμαι για τις δικές μου αλλαγές"
+ label_registration_activation_by_email: ενεργοποίηση λογαριασμού με email
+ label_registration_manual_activation: χειροκίνητη ενεργοποίηση λογαριασμού
+ label_registration_automatic_activation: αυτόματη ενεργοποίηση λογαριασμού
+ label_display_per_page: "Ανά σελίδα: {{value}}"
+ label_age: Ηλικία
+ label_change_properties: Αλλαγή ιδιοτήτων
+ label_general: Γενικά
+ label_more: Περισσότερα
+ label_scm: SCM
+ label_plugins: 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: Εισερχόμενα email
+ 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 ενημερώθηκε
+
+ button_login: Σύνδεση
+ button_submit: Αποστολή
+ button_save: Αποθήκευση
+ button_check_all: Επιλογή όλων
+ button_uncheck_all: Αποεπιλογή όλων
+ button_delete: Διαγραφή
+ button_create: Δημιουργία
+ button_create_and_continue: Δημιουργία και συνέχεια
+ button_test: Τεστ
+ button_edit: Επεξεργασία
+ button_add: Προσθήκη
+ button_change: Αλλαγή
+ button_apply: Εφαρμογή
+ button_clear: Καθαρισμός
+ button_lock: Κλείδωμα
+ button_unlock: Ξεκλείδωμα
+ button_download: Μεταφόρτωση
+ button_list: Λίστα
+ button_view: Προβολή
+ button_move: Μετακίνηση
+ button_back: Πίσω
+ button_cancel: Ακύρωση
+ button_activate: Ενεργοποίηση
+ button_sort: Ταξινόμηση
+ button_log_time: Ιστορικό χρόνου
+ button_rollback: Επαναφορά σε αυτή την έκδοση
+ button_watch: Παρακολούθηση
+ button_unwatch: Αναίρεση παρακολούθησης
+ button_reply: Απάντηση
+ button_archive: Αρχειοθέτηση
+ button_unarchive: Αναίρεση αρχειοθέτησης
+ button_reset: Επαναφορά
+ button_rename: Μετονομασία
+ button_change_password: Αλλαγή κωδικού πρόσβασης
+ button_copy: Αντιγραφή
+ button_annotate: Σχολιασμός
+ button_update: Ενημέρωση
+ button_configure: Ρύθμιση
+ button_quote: Παράθεση
+
+ status_active: ενεργό(ς)/ή
+ status_registered: εγεγγραμμένο(ς)/η
+ status_locked: κλειδωμένο(ς)/η
+
+ text_select_mail_notifications: Επιλογή ενεργειών για τις οποίες θα πρέπει να αποσταλεί ειδοποίηση με email.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 σημαίνει ότι δεν υπάρχουν περιορισμοί
+ text_project_destroy_confirmation: Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το έργο και τα σχετικά δεδομένα του;
+ text_subprojects_destroy_warning: "Επίσης το(α) επιμέρους έργο(α): {{value}} θα διαγραφούν."
+ text_workflow_edit: Επιλέξτε ένα ρόλο και έναν ανιχνευτή για να επεξεργαστείτε τη ροή εργασίας
+ text_are_you_sure: Είστε σίγουρος ;
+ text_tip_task_begin_day: καθήκοντα που ξεκινάνε σήμερα
+ text_tip_task_end_day: καθήκοντα που τελειώνουν σήμερα
+ text_tip_task_begin_end_day: καθήκοντα που ξεκινάνε και τελειώνουν σήμερα
+ text_project_identifier_info: 'Επιτρέπονται μόνο μικρά πεζά γράμματα (a-z), αριθμοί και παύλες. <br /> Μετά την αποθήκευση, το αναγνωριστικό δεν μπορεί να αλλάξει.'
+ text_caracters_maximum: "μέγιστος αριθμός {{count}} χαρακτήρες."
+ text_caracters_minimum: "Πρέπει να περιέχει τουλάχιστον {{count}} χαρακτήρες."
+ text_length_between: "Μήκος μεταξύ {{min}} και {{max}} χαρακτήρες."
+ text_tracker_no_workflow: Δεν έχει οριστεί ροή εργασίας για αυτό τον ανιχνευτή
+ text_unallowed_characters: Μη επιτρεπόμενοι χαρακτήρες
+ text_comma_separated: Επιτρέπονται πολλαπλές τιμές (χωρισμένες με κόμμα).
+ text_issues_ref_in_commit_messages: Αναφορά και καθορισμός θεμάτων σε μηνύματα commit
+ 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: "Εφαρμόστηκε στο changeset {{value}}."
+ text_issues_destroy_confirmation: 'Είστε σίγουρος ότι θέλετε να διαγράψετε το επιλεγμένο θέμα(τα);'
+ text_select_project_modules: 'Επιλέξτε ποιες μονάδες θα ενεργοποιήσετε για αυτό το έργο:'
+ text_default_administrator_account_changed: Ο προκαθορισμένος λογαριασμός του διαχειριστή άλλαξε
+ text_file_repository_writable: Εγγράψιμος κατάλογος συνημμένων
+ text_plugin_assets_writable: Εγγράψιμος κατάλογος plugin assets
+ 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: "Δεν έχουν γίνει ρυθμίσεις παράδοσης email, και οι ειδοποιήσεις είναι απενεργοποιημένες.\nΔηλώστε τον εξυπηρετητή SMTP στο config/email.yml και κάντε επανακκίνηση την εφαρμογή για να τις ρυθμίσεις."
+ text_repository_usernames_mapping: "Επιλέξτε ή ενημερώστε τον χρήστη Redmine που αντιστοιχεί σε κάθε όνομα χρήστη στο ιστορικό του αποθετηρίου.\nΧρήστες με το ίδιο όνομα χρήστη ή email στο Redmine και στο αποθετηρίο αντιστοιχίζονται αυτόματα."
+ text_diff_truncated: '... Αυτό το diff εχεί κοπεί επειδή υπερβαίνει το μέγιστο μέγεθος που μπορεί να προβληθεί.'
+ text_custom_field_possible_values_info: 'Μία γραμμή για κάθε τιμή'
+ text_wiki_page_destroy_question: "Αυτή η σελίδα έχει {{descendants}} σελίδες τέκνων και απογόνων. Τι θέλετε να κάνετε ;"
+ text_wiki_page_nullify_children: "Διατηρήστε τις σελίδες τέκνων ως σελίδες root"
+ text_wiki_page_destroy_children: "Διαγράψτε όλες τις σελίδες τέκνων και των απογόνων τους"
+ text_wiki_page_reassign_children: "Επανεκχώριση των σελίδων τέκνων στη γονική σελίδα"
+
+ default_role_manager: Manager
+ default_role_developper: Developer
+ default_role_reporter: Reporter
+ default_tracker_bug: Σφάλματα
+ default_tracker_feature: Λειτουργίες
+ default_tracker_support: Υποστήριξη
+ default_issue_status_new: Νέα
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Επιλυμένο
+ default_issue_status_feedback: Σχόλια
+ default_issue_status_closed: Κλειστό
+ default_issue_status_rejected: Απορριπτέο
+ default_doc_category_user: Τεκμηρίωση χρήστη
+ default_doc_category_tech: Τεχνική τεκμηρίωση
+ default_priority_low: Χαμηλή
+ default_priority_normal: Κανονική
+ default_priority_high: Υψηλή
+ default_priority_urgent: Επείγον
+ default_priority_immediate: Άμεση
+ default_activity_design: Σχεδιασμός
+ default_activity_development: Ανάπτυξη
+
+ enumeration_issue_priorities: Προτεραιότητα θέματος
+ enumeration_doc_categories: Κατηγορία εγγράφων
+ enumeration_activities: Δραστηριότητες (κατακερματισμός χρόνου)
+ text_journal_changed: "{{label}} άλλαξε από {{old}} σε {{new}}"
+ text_journal_set_to: "{{label}} ορίζεται σε {{value}}"
+ text_journal_deleted: "{{label}} διαγράφηκε ({{old}})"
+ label_group_plural: Ομάδες
+ label_group: Ομάδα
+ label_group_new: Νέα ομάδα
+ label_time_entry_plural: Χρόνος που δαπανήθηκε
+ text_journal_added: "{{label}} {{value}} 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
--- /dev/null
+en:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%m/%d/%Y"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%m/%d/%Y %I:%M %p"
+ time: "%I:%M %p"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ number:
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ 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: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match confirmation"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ too_long: "is too long (maximum is {{count}} characters)"
+ too_short: "is too short (minimum is {{count}} characters)"
+ wrong_length: "is the wrong length (should be {{count}} characters)"
+ taken: "has already been taken"
+ not_a_number: "is not a number"
+ not_a_date: "is not a valid date"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ greater_than_start_date: "must be greater than start date"
+ not_same_project: "doesn't belong to the same project"
+ circular_dependency: "This relation would create a circular dependency"
+
+ actionview_instancetag_blank_option: Please select
+
+ general_text_No: 'No'
+ general_text_Yes: 'Yes'
+ general_text_no: 'no'
+ general_text_yes: 'yes'
+ general_lang_name: 'English'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '7'
+
+ notice_account_updated: Account was successfully updated.
+ notice_account_invalid_creditentials: Invalid user or password
+ notice_account_password_updated: Password was successfully updated.
+ notice_account_wrong_password: Wrong password
+ notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
+ notice_account_unknown_email: Unknown user.
+ notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
+ notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
+ notice_account_activated: Your account has been activated. You can now log in.
+ notice_successful_create: Successful creation.
+ notice_successful_update: Successful update.
+ notice_successful_delete: Successful deletion.
+ notice_successful_connection: Successful connection.
+ notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
+ notice_locking_conflict: Data has been updated by another user.
+ notice_not_authorized: You are not authorized to access this page.
+ notice_email_sent: "An email was sent to {{value}}"
+ notice_email_error: "An error occurred while sending mail ({{value}})"
+ notice_feeds_access_key_reseted: Your RSS access key was reset.
+ notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
+ notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+ notice_account_pending: "Your account was created and is now pending administrator approval."
+ notice_default_data_loaded: Default configuration successfully loaded.
+ notice_unable_delete_version: Unable to delete version.
+
+ error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
+ error_scm_not_found: "The entry or revision was not found in the repository."
+ error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
+ error_scm_annotate: "The entry does not exist or can not be annotated."
+ error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
+ error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
+ error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
+ error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
+
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+
+ mail_subject_lost_password: "Your {{value}} password"
+ mail_body_lost_password: 'To change your password, click on the following link:'
+ mail_subject_register: "Your {{value}} account activation"
+ mail_body_register: 'To activate your account, click on the following link:'
+ mail_body_account_information_external: "You can use your {{value}} account to log in."
+ mail_body_account_information: Your account information
+ mail_subject_account_activation_request: "{{value}} account activation request"
+ mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
+
+ gui_validation_error: 1 error
+ gui_validation_error_plural: "{{count}} errors"
+
+ field_name: Name
+ field_description: Description
+ field_summary: Summary
+ field_is_required: Required
+ field_firstname: Firstname
+ field_lastname: Lastname
+ field_mail: Email
+ field_filename: File
+ field_filesize: Size
+ field_downloads: Downloads
+ field_author: Author
+ field_created_on: Created
+ field_updated_on: Updated
+ field_field_format: Format
+ field_is_for_all: For all projects
+ field_possible_values: Possible values
+ field_regexp: Regular expression
+ field_min_length: Minimum length
+ field_max_length: Maximum length
+ field_value: Value
+ field_category: Category
+ field_title: Title
+ field_project: Project
+ field_issue: Issue
+ field_status: Status
+ field_notes: Notes
+ field_is_closed: Issue closed
+ field_is_default: Default value
+ field_tracker: Tracker
+ field_subject: Subject
+ field_due_date: Due date
+ field_assigned_to: Assigned to
+ field_priority: Priority
+ field_fixed_version: Target version
+ field_user: User
+ field_role: Role
+ field_homepage: Homepage
+ field_is_public: Public
+ field_parent: Subproject of
+ field_is_in_chlog: Issues displayed in changelog
+ field_is_in_roadmap: Issues displayed in roadmap
+ field_login: Login
+ field_mail_notification: Email notifications
+ field_admin: Administrator
+ field_last_login_on: Last connection
+ field_language: Language
+ field_effective_date: Date
+ field_password: Password
+ field_new_password: New password
+ field_password_confirmation: Confirmation
+ field_version: Version
+ field_type: Type
+ field_host: Host
+ field_port: Port
+ field_account: Account
+ field_base_dn: Base DN
+ field_attr_login: Login attribute
+ field_attr_firstname: Firstname attribute
+ field_attr_lastname: Lastname attribute
+ field_attr_mail: Email attribute
+ field_onthefly: On-the-fly user creation
+ field_start_date: Start
+ field_done_ratio: % Done
+ field_auth_source: Authentication mode
+ field_hide_mail: Hide my email address
+ field_comments: Comment
+ field_url: URL
+ field_start_page: Start page
+ field_subproject: Subproject
+ field_hours: Hours
+ field_activity: Activity
+ field_spent_on: Date
+ field_identifier: Identifier
+ field_is_filter: Used as a filter
+ field_issue_to: Related issue
+ field_delay: Delay
+ field_assignable: Issues can be assigned to this role
+ field_redirect_existing_links: Redirect existing links
+ field_estimated_hours: Estimated time
+ field_column_names: Columns
+ field_time_zone: Time zone
+ field_searchable: Searchable
+ field_default_value: Default value
+ field_comments_sorting: Display comments
+ field_parent_title: Parent page
+ field_editable: Editable
+ field_watcher: Watcher
+ field_identity_url: OpenID URL
+ field_content: Content
+ field_group_by: Group results by
+
+ setting_app_title: Application title
+ setting_app_subtitle: Application subtitle
+ setting_welcome_text: Welcome text
+ setting_default_language: Default language
+ setting_login_required: Authentication required
+ setting_self_registration: Self-registration
+ setting_attachment_max_size: Attachment max. size
+ setting_issues_export_limit: Issues export limit
+ setting_mail_from: Emission email address
+ setting_bcc_recipients: Blind carbon copy recipients (bcc)
+ setting_plain_text_mail: Plain text mail (no HTML)
+ setting_host_name: Host name and path
+ setting_text_formatting: Text formatting
+ setting_wiki_compression: Wiki history compression
+ setting_feeds_limit: Feed content limit
+ setting_default_projects_public: New projects are public by default
+ setting_autofetch_changesets: Autofetch commits
+ setting_sys_api_enabled: Enable WS for repository management
+ setting_commit_ref_keywords: Referencing keywords
+ setting_commit_fix_keywords: Fixing keywords
+ setting_autologin: Autologin
+ setting_date_format: Date format
+ setting_time_format: Time format
+ setting_cross_project_issue_relations: Allow cross-project issue relations
+ setting_issue_list_default_columns: Default columns displayed on the issue list
+ setting_repositories_encodings: Repositories encodings
+ setting_commit_logs_encoding: Commit messages encoding
+ setting_emails_footer: Emails footer
+ setting_protocol: Protocol
+ setting_per_page_options: Objects per page options
+ setting_user_format: Users display format
+ setting_activity_days_default: Days displayed on project activity
+ setting_display_subprojects_issues: Display subprojects issues on main projects by default
+ 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: Generate sequential project identifiers
+ setting_gravatar_enabled: Use Gravatar user icons
+ setting_gravatar_default: Default Gravatar image
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_openid: Allow OpenID login and registration
+ setting_password_min_length: Minimum password length
+ setting_new_project_user_role_id: Role given to a non-admin user who creates a project
+ setting_default_projects_modules: Default enabled modules for new projects
+
+ permission_add_project: Create project
+ permission_edit_project: Edit project
+ permission_select_project_modules: Select project modules
+ permission_manage_members: Manage members
+ permission_manage_versions: Manage versions
+ permission_manage_categories: Manage issue categories
+ permission_add_issues: Add issues
+ permission_edit_issues: Edit issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_add_issue_notes: Add notes
+ permission_edit_issue_notes: Edit notes
+ permission_edit_own_issue_notes: Edit own notes
+ permission_move_issues: Move issues
+ permission_delete_issues: Delete issues
+ permission_manage_public_queries: Manage public queries
+ permission_save_queries: Save queries
+ permission_view_gantt: View gantt chart
+ permission_view_calendar: View calendar
+ permission_view_issue_watchers: View watchers list
+ permission_add_issue_watchers: Add watchers
+ permission_delete_issue_watchers: Delete watchers
+ permission_log_time: Log spent time
+ permission_view_time_entries: View spent time
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_news: Manage news
+ permission_comment_news: Comment news
+ permission_manage_documents: Manage documents
+ permission_view_documents: View documents
+ permission_manage_files: Manage files
+ permission_view_files: View files
+ permission_manage_wiki: Manage wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_view_wiki_pages: View wiki
+ permission_view_wiki_edits: View wiki history
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_manage_repository: Manage repository
+ permission_browse_repository: Browse repository
+ permission_view_changesets: View changesets
+ permission_commit_access: Commit access
+ permission_manage_boards: Manage boards
+ permission_view_messages: View messages
+ permission_add_messages: Post messages
+ permission_edit_messages: Edit messages
+ permission_edit_own_messages: Edit own messages
+ permission_delete_messages: Delete messages
+ permission_delete_own_messages: Delete own messages
+
+ project_module_issue_tracking: Issue tracking
+ project_module_time_tracking: Time tracking
+ project_module_news: News
+ project_module_documents: Documents
+ project_module_files: Files
+ project_module_wiki: Wiki
+ project_module_repository: Repository
+ project_module_boards: Boards
+
+ label_user: User
+ label_user_plural: Users
+ label_user_new: New user
+ label_user_anonymous: Anonymous
+ label_project: Project
+ label_project_new: New project
+ label_project_plural: Projects
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: All Projects
+ label_project_latest: Latest projects
+ label_issue: Issue
+ label_issue_new: New issue
+ label_issue_plural: Issues
+ label_issue_view_all: View all issues
+ label_issues_by: "Issues by {{value}}"
+ label_issue_added: Issue added
+ label_issue_updated: Issue updated
+ label_document: Document
+ label_document_new: New document
+ label_document_plural: Documents
+ label_document_added: Document added
+ label_role: Role
+ label_role_plural: Roles
+ label_role_new: New role
+ label_role_and_permissions: Roles and permissions
+ label_member: Member
+ label_member_new: New member
+ label_member_plural: Members
+ label_tracker: Tracker
+ label_tracker_plural: Trackers
+ label_tracker_new: New tracker
+ label_workflow: Workflow
+ label_issue_status: Issue status
+ label_issue_status_plural: Issue statuses
+ label_issue_status_new: New status
+ label_issue_category: Issue category
+ label_issue_category_plural: Issue categories
+ label_issue_category_new: New category
+ label_custom_field: Custom field
+ label_custom_field_plural: Custom fields
+ label_custom_field_new: New custom field
+ label_enumerations: Enumerations
+ label_enumeration_new: New value
+ label_information: Information
+ label_information_plural: Information
+ label_please_login: Please log in
+ label_register: Register
+ label_login_with_open_id_option: or login with OpenID
+ label_password_lost: Lost password
+ label_home: Home
+ label_my_page: My page
+ label_my_account: My account
+ label_my_projects: My projects
+ label_administration: Administration
+ label_login: Sign in
+ label_logout: Sign out
+ label_help: Help
+ label_reported_issues: Reported issues
+ label_assigned_to_me_issues: Issues assigned to me
+ label_last_login: Last connection
+ label_registered_on: Registered on
+ label_activity: Activity
+ label_overall_activity: Overall activity
+ label_user_activity: "{{value}}'s activity"
+ label_new: New
+ label_logged_as: Logged in as
+ label_environment: Environment
+ label_authentication: Authentication
+ label_auth_source: Authentication mode
+ label_auth_source_new: New authentication mode
+ label_auth_source_plural: Authentication modes
+ label_subproject_plural: Subprojects
+ label_and_its_subprojects: "{{value}} and its subprojects"
+ label_min_max_length: Min - Max length
+ label_list: List
+ label_date: Date
+ label_integer: Integer
+ label_float: Float
+ label_boolean: Boolean
+ label_string: Text
+ label_text: Long text
+ label_attribute: Attribute
+ label_attribute_plural: Attributes
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: No data to display
+ label_change_status: Change status
+ label_history: History
+ label_attachment: File
+ label_attachment_new: New file
+ label_attachment_delete: Delete file
+ label_attachment_plural: Files
+ label_file_added: File added
+ label_report: Report
+ label_report_plural: Reports
+ label_news: News
+ label_news_new: Add news
+ label_news_plural: News
+ label_news_latest: Latest news
+ label_news_view_all: View all news
+ label_news_added: News added
+ label_change_log: Change log
+ label_settings: Settings
+ label_overview: Overview
+ label_version: Version
+ label_version_new: New version
+ label_version_plural: Versions
+ label_confirmation: Confirmation
+ label_export_to: 'Also available in:'
+ label_read: Read...
+ label_public_projects: Public projects
+ label_open_issues: open
+ label_open_issues_plural: open
+ label_closed_issues: closed
+ label_closed_issues_plural: closed
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Total
+ label_permissions: Permissions
+ label_current_status: Current status
+ label_new_statuses_allowed: New statuses allowed
+ label_all: all
+ label_none: none
+ label_nobody: nobody
+ label_next: Next
+ label_previous: Previous
+ label_used_by: Used by
+ label_details: Details
+ label_add_note: Add a note
+ label_per_page: Per page
+ label_calendar: Calendar
+ label_months_from: months from
+ label_gantt: Gantt
+ label_internal: Internal
+ label_last_changes: "last {{count}} changes"
+ label_change_view_all: View all changes
+ label_personalize_page: Personalize this page
+ label_comment: Comment
+ label_comment_plural: Comments
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Add a comment
+ label_comment_added: Comment added
+ label_comment_delete: Delete comments
+ label_query: Custom query
+ label_query_plural: Custom queries
+ label_query_new: New query
+ label_filter_add: Add filter
+ label_filter_plural: Filters
+ label_equals: is
+ label_not_equals: is not
+ label_in_less_than: in less than
+ label_in_more_than: in more than
+ label_greater_or_equal: '>='
+ label_less_or_equal: '<='
+ label_in: in
+ label_today: today
+ label_all_time: all time
+ label_yesterday: yesterday
+ label_this_week: this week
+ label_last_week: last week
+ label_last_n_days: "last {{count}} days"
+ label_this_month: this month
+ label_last_month: last month
+ label_this_year: this year
+ label_date_range: Date range
+ label_less_than_ago: less than days ago
+ label_more_than_ago: more than days ago
+ label_ago: days ago
+ label_contains: contains
+ label_not_contains: doesn't contain
+ label_day_plural: days
+ label_repository: Repository
+ label_repository_plural: Repositories
+ label_browse: Browse
+ label_modification: "{{count}} change"
+ label_modification_plural: "{{count}} changes"
+ label_branch: Branch
+ label_tag: Tag
+ label_revision: Revision
+ label_revision_plural: Revisions
+ label_associated_revisions: Associated revisions
+ label_added: added
+ label_modified: modified
+ label_copied: copied
+ label_renamed: renamed
+ label_deleted: deleted
+ label_latest_revision: Latest revision
+ label_latest_revision_plural: Latest revisions
+ label_view_revisions: View revisions
+ label_view_all_revisions: View all revisions
+ label_max_size: Maximum size
+ label_sort_highest: Move to top
+ label_sort_higher: Move up
+ label_sort_lower: Move down
+ label_sort_lowest: Move to bottom
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Due in {{value}}"
+ label_roadmap_overdue: "{{value}} late"
+ label_roadmap_no_issues: No issues for this version
+ label_search: Search
+ label_result_plural: Results
+ label_all_words: All words
+ label_wiki: Wiki
+ label_wiki_edit: Wiki edit
+ label_wiki_edit_plural: Wiki edits
+ label_wiki_page: Wiki page
+ label_wiki_page_plural: Wiki pages
+ label_index_by_title: Index by title
+ label_index_by_date: Index by date
+ label_current_version: Current version
+ label_preview: Preview
+ label_feed_plural: Feeds
+ label_changes_details: Details of all changes
+ label_issue_tracking: Issue tracking
+ label_spent_time: Spent time
+ label_f_hour: "{{value}} hour"
+ label_f_hour_plural: "{{value}} hours"
+ label_time_tracking: Time tracking
+ label_change_plural: Changes
+ label_statistics: Statistics
+ label_commits_per_month: Commits per month
+ label_commits_per_author: Commits per author
+ label_view_diff: View differences
+ label_diff_inline: inline
+ label_diff_side_by_side: side by side
+ label_options: Options
+ label_copy_workflow_from: Copy workflow from
+ label_permissions_report: Permissions report
+ label_watched_issues: Watched issues
+ label_related_issues: Related issues
+ label_applied_status: Applied status
+ label_loading: Loading...
+ label_relation_new: New relation
+ label_relation_delete: Delete relation
+ label_relates_to: related to
+ label_duplicates: duplicates
+ label_duplicated_by: duplicated by
+ label_blocks: blocks
+ label_blocked_by: blocked by
+ label_precedes: precedes
+ label_follows: follows
+ label_end_to_start: end to start
+ label_end_to_end: end to end
+ label_start_to_start: start to start
+ label_start_to_end: start to end
+ label_stay_logged_in: Stay logged in
+ label_disabled: disabled
+ label_show_completed_versions: Show completed versions
+ label_me: me
+ label_board: Forum
+ label_board_new: New forum
+ label_board_plural: Forums
+ label_topic_plural: Topics
+ label_message_plural: Messages
+ label_message_last: Last message
+ label_message_new: New message
+ label_message_posted: Message added
+ label_reply_plural: Replies
+ label_send_information: Send account information to the user
+ label_year: Year
+ label_month: Month
+ label_week: Week
+ label_date_from: From
+ label_date_to: To
+ label_language_based: Based on user's language
+ label_sort_by: "Sort by {{value}}"
+ label_send_test_email: Send a test email
+ label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
+ label_module_plural: Modules
+ label_added_time_by: "Added by {{author}} {{age}} ago"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ label_updated_time: "Updated {{value}} ago"
+ label_jump_to_a_project: Jump to a project...
+ label_file_plural: Files
+ label_changeset_plural: Changesets
+ label_default_columns: Default columns
+ label_no_change_option: (No change)
+ label_bulk_edit_selected_issues: Bulk edit selected issues
+ label_theme: Theme
+ label_default: Default
+ label_search_titles_only: Search titles only
+ label_user_mail_option_all: "For any event on all my projects"
+ label_user_mail_option_selected: "For any event on the selected projects only..."
+ label_user_mail_option_none: "Only for things I watch or I'm involved in"
+ label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
+ label_registration_activation_by_email: account activation by email
+ label_registration_manual_activation: manual account activation
+ label_registration_automatic_activation: automatic account activation
+ label_display_per_page: "Per page: {{value}}"
+ label_age: Age
+ label_change_properties: Change properties
+ label_general: General
+ label_more: More
+ label_scm: SCM
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP authentication
+ label_downloads_abbr: D/L
+ label_optional_description: Optional description
+ label_add_another_file: Add another file
+ label_preferences: Preferences
+ label_chronological_order: In chronological order
+ label_reverse_chronological_order: In reverse chronological order
+ label_planning: Planning
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ label_issue_watchers: Watchers
+ label_example: Example
+ label_display: Display
+ label_sort: Sort
+ label_ascending: Ascending
+ label_descending: Descending
+ label_date_from_to: From {{start}} to {{end}}
+ label_wiki_content_added: Wiki page added
+ label_wiki_content_updated: Wiki page updated
+ label_group: Group
+ label_group_plural: Groups
+ label_group_new: New group
+ label_time_entry_plural: Spent time
+
+ button_login: Login
+ button_submit: Submit
+ button_save: Save
+ button_check_all: Check all
+ button_uncheck_all: Uncheck all
+ button_delete: Delete
+ button_create: Create
+ button_create_and_continue: Create and continue
+ button_test: Test
+ button_edit: Edit
+ button_add: Add
+ button_change: Change
+ button_apply: Apply
+ button_clear: Clear
+ button_lock: Lock
+ button_unlock: Unlock
+ button_download: Download
+ button_list: List
+ button_view: View
+ button_move: Move
+ button_move_and_follow: Move and follow
+ button_back: Back
+ button_cancel: Cancel
+ button_activate: Activate
+ button_sort: Sort
+ button_log_time: Log time
+ button_rollback: Rollback to this version
+ button_watch: Watch
+ button_unwatch: Unwatch
+ button_reply: Reply
+ button_archive: Archive
+ button_unarchive: Unarchive
+ button_reset: Reset
+ button_rename: Rename
+ button_change_password: Change password
+ button_copy: Copy
+ button_annotate: Annotate
+ button_update: Update
+ button_configure: Configure
+ button_quote: Quote
+
+ status_active: active
+ status_registered: registered
+ status_locked: locked
+
+ version_status_open: open
+ version_status_locked: locked
+ version_status_closed: closed
+
+ field_active: Active
+
+ text_select_mail_notifications: Select actions for which email notifications should be sent.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 means no restriction
+ text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
+ text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
+ text_workflow_edit: Select a role and a tracker to edit the workflow
+ text_are_you_sure: Are you sure ?
+ text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
+ text_journal_set_to: "{{label}} set to {{value}}"
+ text_journal_deleted: "{{label}} deleted ({{old}})"
+ text_journal_added: "{{label}} {{value}} added"
+ text_tip_task_begin_day: task beginning this day
+ text_tip_task_end_day: task ending this day
+ text_tip_task_begin_end_day: task beginning and ending this day
+ text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
+ text_caracters_maximum: "{{count}} characters maximum."
+ text_caracters_minimum: "Must be at least {{count}} characters long."
+ text_length_between: "Length between {{min}} and {{max}} characters."
+ text_tracker_no_workflow: No workflow defined for this tracker
+ text_unallowed_characters: Unallowed characters
+ text_comma_separated: Multiple values allowed (comma separated).
+ text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{author}}."
+ text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
+ text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
+ text_issue_category_destroy_assignments: Remove category assignments
+ text_issue_category_reassign_to: Reassign issues to this category
+ text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
+ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+ text_load_default_configuration: Load the default configuration
+ text_status_changed_by_changeset: "Applied in changeset {{value}}."
+ text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+ text_select_project_modules: 'Select modules to enable for this project:'
+ text_default_administrator_account_changed: Default administrator account changed
+ text_file_repository_writable: Attachments directory writable
+ text_plugin_assets_writable: Plugin assets directory writable
+ text_rmagick_available: RMagick available (optional)
+ text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
+ text_destroy_time_entries: Delete reported hours
+ text_assign_time_entries_to_project: Assign reported hours to the project
+ text_reassign_time_entries: 'Reassign reported hours to this issue:'
+ text_user_wrote: "{{value}} wrote:"
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ text_custom_field_possible_values_info: 'One line for each value'
+ text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
+ text_wiki_page_nullify_children: "Keep child pages as root pages"
+ text_wiki_page_destroy_children: "Delete child pages and all their descendants"
+ text_wiki_page_reassign_children: "Reassign child pages to this parent page"
+
+ default_role_manager: Manager
+ default_role_developper: Developer
+ default_role_reporter: Reporter
+ default_tracker_bug: Bug
+ default_tracker_feature: Feature
+ default_tracker_support: Support
+ default_issue_status_new: New
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Resolved
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Closed
+ default_issue_status_rejected: Rejected
+ default_doc_category_user: User documentation
+ default_doc_category_tech: Technical documentation
+ default_priority_low: Low
+ default_priority_normal: Normal
+ default_priority_high: High
+ default_priority_urgent: Urgent
+ default_priority_immediate: Immediate
+ default_activity_design: Design
+ default_activity_development: Development
+
+ enumeration_issue_priorities: Issue priorities
+ enumeration_doc_categories: Document categories
+ enumeration_activities: Activities (time tracking)
+ enumeration_system_activity: System Activity
--- /dev/null
+# Spanish translations for Rails
+# by Francisco Fernando García Nieto (ffgarcianieto@gmail.com)
+
+es:
+ number:
+ # Used in number_with_delimiter()
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format:
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ","
+ # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: "."
+ # Number of decimals, behind the separator (1 with a precision of 2 gives: 1.00)
+ precision: 3
+
+ # Used in number_to_currency()
+ currency:
+ format:
+ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
+ format: "%n %u"
+ unit: "€"
+ # These three are to override number.format and are optional
+ separator: ","
+ delimiter: "."
+ precision: 2
+
+ # Used in number_to_percentage()
+ percentage:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_precision()
+ precision:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_human_size()
+ human:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
+ datetime:
+ distance_in_words:
+ half_a_minute: "medio minuto"
+ less_than_x_seconds:
+ one: "menos de 1 segundo"
+ other: "menos de {{count}} segundos"
+ x_seconds:
+ one: "1 segundo"
+ other: "{{count}} segundos"
+ less_than_x_minutes:
+ one: "menos de 1 minuto"
+ other: "menos de {{count}} minutos"
+ x_minutes:
+ one: "1 minuto"
+ other: "{{count}} minutos"
+ about_x_hours:
+ one: "alrededor de 1 hora"
+ other: "alrededor de {{count}} horas"
+ x_days:
+ one: "1 día"
+ other: "{{count}} días"
+ about_x_months:
+ one: "alrededor de 1 mes"
+ other: "alrededor de {{count}} meses"
+ x_months:
+ one: "1 mes"
+ other: "{{count}} meses"
+ about_x_years:
+ one: "alrededor de 1 año"
+ other: "alrededor de {{count}} años"
+ over_x_years:
+ one: "más de 1 año"
+ other: "más de {{count}} años"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "no se pudo guardar este {{model}} porque se encontró 1 error"
+ other: "no se pudo guardar este {{model}} porque se encontraron {{count}} errores"
+ # The variable :count is also available
+ body: "Se encontraron problemas con los siguientes campos:"
+
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "no está incluido en la lista"
+ exclusion: "está reservado"
+ invalid: "no es válido"
+ confirmation: "no coincide con la confirmación"
+ accepted: "debe ser aceptado"
+ empty: "no puede estar vacío"
+ blank: "no puede estar en blanco"
+ too_long: "es demasiado largo ({{count}} caracteres máximo)"
+ too_short: "es demasiado corto ({{count}} caracteres mínimo)"
+ wrong_length: "no tiene la longitud correcta ({{count}} caracteres exactos)"
+ taken: "ya está en uso"
+ not_a_number: "no es un número"
+ greater_than: "debe ser mayor que {{count}}"
+ greater_than_or_equal_to: "debe ser mayor que o igual a {{count}}"
+ equal_to: "debe ser igual a {{count}}"
+ less_than: "debe ser menor que {{count}}"
+ less_than_or_equal_to: "debe ser menor que o igual a {{count}}"
+ odd: "debe ser impar"
+ even: "debe ser par"
+ greater_than_start_date: "debe ser posterior a la fecha de comienzo"
+ not_same_project: "no pertenece al mismo proyecto"
+ circular_dependency: "Esta relación podría crear una dependencia circular"
+
+ # Append your own errors here or at the model/attributes scope.
+
+ models:
+ # Overrides default messages
+
+ attributes:
+ # Overrides model and default messages.
+
+ 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: "%d de %b"
+ long: "%d de %B de %Y"
+
+ day_names: [Domingo, Lunes, Martes, Miércoles, Jueves, Viernes, Sábado]
+ abbr_day_names: [Dom, Lun, Mar, Mie, Jue, Vie, Sab]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio, Agosto, Setiembre, Octubre, Noviembre, Diciembre]
+ abbr_month_names: [~, Ene, Feb, Mar, Abr, May, Jun, Jul, Ago, Set, Oct, Nov, Dic]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%A, %d de %B de %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d de %b %H:%M"
+ long: "%d de %B de %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "y"
+
+ actionview_instancetag_blank_option: Por favor seleccione
+
+ button_activate: Activar
+ button_add: Añadir
+ button_annotate: Anotar
+ button_apply: Aceptar
+ button_archive: Archivar
+ button_back: Atrás
+ button_cancel: Cancelar
+ button_change: Cambiar
+ button_change_password: Cambiar contraseña
+ button_check_all: Seleccionar todo
+ button_clear: Anular
+ button_configure: Configurar
+ button_copy: Copiar
+ button_create: Crear
+ button_delete: Borrar
+ button_download: Descargar
+ button_edit: Modificar
+ button_list: Listar
+ button_lock: Bloquear
+ button_log_time: Tiempo dedicado
+ button_login: Conexión
+ button_move: Mover
+ button_quote: Citar
+ button_rename: Renombrar
+ button_reply: Responder
+ button_reset: Reestablecer
+ button_rollback: Volver a esta versión
+ button_save: Guardar
+ button_sort: Ordenar
+ button_submit: Aceptar
+ button_test: Probar
+ button_unarchive: Desarchivar
+ button_uncheck_all: No seleccionar nada
+ button_unlock: Desbloquear
+ button_unwatch: No monitorizar
+ button_update: Actualizar
+ button_view: Ver
+ button_watch: Monitorizar
+ default_activity_design: Diseño
+ default_activity_development: Desarrollo
+ default_doc_category_tech: Documentación técnica
+ default_doc_category_user: Documentación de usuario
+ default_issue_status_in_progress: In Progress
+ default_issue_status_closed: Cerrada
+ default_issue_status_feedback: Comentarios
+ default_issue_status_new: Nueva
+ default_issue_status_rejected: Rechazada
+ default_issue_status_resolved: Resuelta
+ default_priority_high: Alta
+ default_priority_immediate: Inmediata
+ default_priority_low: Baja
+ default_priority_normal: Normal
+ default_priority_urgent: Urgente
+ default_role_developper: Desarrollador
+ default_role_manager: Jefe de proyecto
+ default_role_reporter: Informador
+ default_tracker_bug: Errores
+ default_tracker_feature: Tareas
+ default_tracker_support: Soporte
+ enumeration_activities: Actividades (tiempo dedicado)
+ enumeration_doc_categories: Categorías del documento
+ enumeration_issue_priorities: Prioridad de las peticiones
+ error_can_t_load_default_data: "No se ha podido cargar la configuración por defecto: {{value}}"
+ error_issue_not_found_in_project: 'La petición no se encuentra o no está asociada a este proyecto'
+ error_scm_annotate: "No existe la entrada o no ha podido ser anotada"
+ error_scm_command_failed: "Se produjo un error al acceder al repositorio: {{value}}"
+ error_scm_not_found: "La entrada y/o la revisión no existe en el repositorio."
+ field_account: Cuenta
+ field_activity: Actividad
+ field_admin: Administrador
+ field_assignable: Se pueden asignar peticiones a este perfil
+ field_assigned_to: Asignado a
+ field_attr_firstname: Cualidad del nombre
+ field_attr_lastname: Cualidad del apellido
+ field_attr_login: Cualidad del identificador
+ field_attr_mail: Cualidad del Email
+ field_auth_source: Modo de identificación
+ field_author: Autor
+ field_base_dn: DN base
+ field_category: Categoría
+ field_column_names: Columnas
+ field_comments: Comentario
+ field_comments_sorting: Mostrar comentarios
+ field_created_on: Creado
+ field_default_value: Estado por defecto
+ field_delay: Retraso
+ field_description: Descripción
+ field_done_ratio: % Realizado
+ field_downloads: Descargas
+ field_due_date: Fecha fin
+ field_effective_date: Fecha
+ field_estimated_hours: Tiempo estimado
+ field_field_format: Formato
+ field_filename: Fichero
+ field_filesize: Tamaño
+ field_firstname: Nombre
+ field_fixed_version: Versión prevista
+ field_hide_mail: Ocultar mi dirección de correo
+ field_homepage: Sitio web
+ field_host: Anfitrión
+ field_hours: Horas
+ field_identifier: Identificador
+ field_is_closed: Petición resuelta
+ field_is_default: Estado por defecto
+ field_is_filter: Usado como filtro
+ field_is_for_all: Para todos los proyectos
+ field_is_in_chlog: Consultar las peticiones en el histórico
+ field_is_in_roadmap: Consultar las peticiones en la planificación
+ field_is_public: Público
+ field_is_required: Obligatorio
+ field_issue: Petición
+ field_issue_to: Petición relacionada
+ field_language: Idioma
+ field_last_login_on: Última conexión
+ field_lastname: Apellido
+ field_login: Identificador
+ field_mail: Correo electrónico
+ field_mail_notification: Notificaciones por correo
+ field_max_length: Longitud máxima
+ field_min_length: Longitud mínima
+ field_name: Nombre
+ field_new_password: Nueva contraseña
+ field_notes: Notas
+ field_onthefly: Creación del usuario "al vuelo"
+ field_parent: Proyecto padre
+ field_parent_title: Página padre
+ field_password: Contraseña
+ field_password_confirmation: Confirmación
+ field_port: Puerto
+ field_possible_values: Valores posibles
+ field_priority: Prioridad
+ field_project: Proyecto
+ field_redirect_existing_links: Redireccionar enlaces existentes
+ field_regexp: Expresión regular
+ field_role: Perfil
+ field_searchable: Incluir en las búsquedas
+ field_spent_on: Fecha
+ field_start_date: Fecha de inicio
+ field_start_page: Página principal
+ field_status: Estado
+ field_subject: Tema
+ field_subproject: Proyecto secundario
+ field_summary: Resumen
+ field_time_zone: Zona horaria
+ field_title: Título
+ field_tracker: Tipo
+ field_type: Tipo
+ field_updated_on: Actualizado
+ field_url: URL
+ field_user: Usuario
+ field_value: Valor
+ field_version: Versión
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-15
+ general_csv_separator: ';'
+ general_first_day_of_week: '1'
+ general_lang_name: 'Español'
+ general_pdf_encoding: ISO-8859-15
+ general_text_No: 'No'
+ general_text_Yes: 'Sí'
+ general_text_no: 'no'
+ general_text_yes: 'sí'
+ gui_validation_error: 1 error
+ gui_validation_error_plural: "{{count}} errores"
+ label_activity: Actividad
+ label_add_another_file: Añadir otro fichero
+ label_add_note: Añadir una nota
+ label_added: añadido
+ label_added_time_by: "Añadido por {{author}} hace {{age}}"
+ label_administration: Administración
+ label_age: Edad
+ label_ago: hace
+ label_all: todos
+ label_all_time: todo el tiempo
+ label_all_words: Todas las palabras
+ label_and_its_subprojects: "{{value}} y proyectos secundarios"
+ label_applied_status: Aplicar estado
+ label_assigned_to_me_issues: Peticiones que me están asignadas
+ label_associated_revisions: Revisiones asociadas
+ label_attachment: Fichero
+ label_attachment_delete: Borrar el fichero
+ label_attachment_new: Nuevo fichero
+ label_attachment_plural: Ficheros
+ label_attribute: Cualidad
+ label_attribute_plural: Cualidades
+ label_auth_source: Modo de autenticación
+ label_auth_source_new: Nuevo modo de autenticación
+ label_auth_source_plural: Modos de autenticación
+ label_authentication: Autenticación
+ label_blocked_by: bloqueado por
+ label_blocks: bloquea a
+ label_board: Foro
+ label_board_new: Nuevo foro
+ label_board_plural: Foros
+ label_boolean: Booleano
+ label_browse: Hojear
+ label_bulk_edit_selected_issues: Editar las peticiones seleccionadas
+ label_calendar: Calendario
+ label_change_log: Cambios
+ label_change_plural: Cambios
+ label_change_properties: Cambiar propiedades
+ label_change_status: Cambiar el estado
+ label_change_view_all: Ver todos los cambios
+ label_changes_details: Detalles de todos los cambios
+ label_changeset_plural: Cambios
+ label_chronological_order: En orden cronológico
+ label_closed_issues: cerrada
+ label_closed_issues_plural: cerradas
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_comment: Comentario
+ label_comment_add: Añadir un comentario
+ label_comment_added: Comentario añadido
+ label_comment_delete: Borrar comentarios
+ label_comment_plural: Comentarios
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_commits_per_author: Commits por autor
+ label_commits_per_month: Commits por mes
+ label_confirmation: Confirmación
+ label_contains: contiene
+ label_copied: copiado
+ label_copy_workflow_from: Copiar flujo de trabajo desde
+ label_current_status: Estado actual
+ label_current_version: Versión actual
+ label_custom_field: Campo personalizado
+ label_custom_field_new: Nuevo campo personalizado
+ label_custom_field_plural: Campos personalizados
+ label_date: Fecha
+ label_date_from: Desde
+ label_date_range: Rango de fechas
+ label_date_to: Hasta
+ label_day_plural: días
+ label_default: Por defecto
+ label_default_columns: Columnas por defecto
+ label_deleted: suprimido
+ label_details: Detalles
+ label_diff_inline: en línea
+ label_diff_side_by_side: cara a cara
+ label_disabled: deshabilitado
+ label_display_per_page: "Por página: {{value}}"
+ label_document: Documento
+ label_document_added: Documento añadido
+ label_document_new: Nuevo documento
+ label_document_plural: Documentos
+ label_download: "{{count}} Descarga"
+ label_download_plural: "{{count}} Descargas"
+ label_downloads_abbr: D/L
+ label_duplicated_by: duplicada por
+ label_duplicates: duplicada de
+ label_end_to_end: fin a fin
+ label_end_to_start: fin a principio
+ label_enumeration_new: Nuevo valor
+ label_enumerations: Listas de valores
+ label_environment: Entorno
+ label_equals: igual
+ label_example: Ejemplo
+ label_export_to: 'Exportar a:'
+ label_f_hour: "{{value}} hora"
+ label_f_hour_plural: "{{value}} horas"
+ label_feed_plural: Feeds
+ label_feeds_access_key_created_on: "Clave de acceso por RSS creada hace {{value}}"
+ label_file_added: Fichero añadido
+ label_file_plural: Archivos
+ label_filter_add: Añadir el filtro
+ label_filter_plural: Filtros
+ label_float: Flotante
+ label_follows: posterior a
+ label_gantt: Gantt
+ label_general: General
+ label_generate_key: Generar clave
+ label_help: Ayuda
+ label_history: Histórico
+ label_home: Inicio
+ label_in: en
+ label_in_less_than: en menos que
+ label_in_more_than: en más que
+ label_incoming_emails: Correos entrantes
+ label_index_by_date: Índice por fecha
+ label_index_by_title: Índice por título
+ label_information: Información
+ label_information_plural: Información
+ label_integer: Número
+ label_internal: Interno
+ label_issue: Petición
+ label_issue_added: Petición añadida
+ label_issue_category: Categoría de las peticiones
+ label_issue_category_new: Nueva categoría
+ label_issue_category_plural: Categorías de las peticiones
+ label_issue_new: Nueva petición
+ label_issue_plural: Peticiones
+ label_issue_status: Estado de la petición
+ label_issue_status_new: Nuevo estado
+ label_issue_status_plural: Estados de las peticiones
+ label_issue_tracking: Peticiones
+ label_issue_updated: Petición actualizada
+ label_issue_view_all: Ver todas las peticiones
+ label_issue_watchers: Seguidores
+ label_issues_by: "Peticiones por {{value}}"
+ label_jump_to_a_project: Ir al proyecto...
+ label_language_based: Basado en el idioma
+ label_last_changes: "últimos {{count}} cambios"
+ label_last_login: Última conexión
+ label_last_month: último mes
+ label_last_n_days: "últimos {{count}} días"
+ label_last_week: última semana
+ label_latest_revision: Última revisión
+ label_latest_revision_plural: Últimas revisiones
+ label_ldap_authentication: Autenticación LDAP
+ label_less_than_ago: hace menos de
+ label_list: Lista
+ label_loading: Cargando...
+ label_logged_as: Conectado como
+ label_login: Conexión
+ label_logout: Desconexión
+ label_max_size: Tamaño máximo
+ label_me: yo mismo
+ label_member: Miembro
+ label_member_new: Nuevo miembro
+ label_member_plural: Miembros
+ label_message_last: Último mensaje
+ label_message_new: Nuevo mensaje
+ label_message_plural: Mensajes
+ label_message_posted: Mensaje añadido
+ label_min_max_length: Longitud mín - máx
+ label_modification: "{{count}} modificación"
+ label_modification_plural: "{{count}} modificaciones"
+ label_modified: modificado
+ label_module_plural: Módulos
+ label_month: Mes
+ label_months_from: meses de
+ label_more: Más
+ label_more_than_ago: hace más de
+ label_my_account: Mi cuenta
+ label_my_page: Mi página
+ label_my_projects: Mis proyectos
+ label_new: Nuevo
+ label_new_statuses_allowed: Nuevos estados autorizados
+ label_news: Noticia
+ label_news_added: Noticia añadida
+ label_news_latest: Últimas noticias
+ label_news_new: Nueva noticia
+ label_news_plural: Noticias
+ label_news_view_all: Ver todas las noticias
+ label_next: Siguiente
+ label_no_change_option: (Sin cambios)
+ label_no_data: Ningún dato a mostrar
+ label_nobody: nadie
+ label_none: ninguno
+ label_not_contains: no contiene
+ label_not_equals: no igual
+ label_open_issues: abierta
+ label_open_issues_plural: abiertas
+ label_optional_description: Descripción opcional
+ label_options: Opciones
+ label_overall_activity: Actividad global
+ label_overview: Vistazo
+ label_password_lost: ¿Olvidaste la contraseña?
+ label_per_page: Por página
+ label_permissions: Permisos
+ label_permissions_report: Informe de permisos
+ label_personalize_page: Personalizar esta página
+ label_planning: Planificación
+ label_please_login: Conexión
+ label_plugins: Extensiones
+ label_precedes: anterior a
+ label_preferences: Preferencias
+ label_preview: Previsualizar
+ label_previous: Anterior
+ label_project: Proyecto
+ label_project_all: Todos los proyectos
+ label_project_latest: Últimos proyectos
+ label_project_new: Nuevo proyecto
+ label_project_plural: Proyectos
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_public_projects: Proyectos públicos
+ label_query: Consulta personalizada
+ label_query_new: Nueva consulta
+ label_query_plural: Consultas personalizadas
+ label_read: Leer...
+ label_register: Registrar
+ label_registered_on: Inscrito el
+ label_registration_activation_by_email: activación de cuenta por correo
+ label_registration_automatic_activation: activación automática de cuenta
+ label_registration_manual_activation: activación manual de cuenta
+ label_related_issues: Peticiones relacionadas
+ label_relates_to: relacionada con
+ label_relation_delete: Eliminar relación
+ label_relation_new: Nueva relación
+ label_renamed: renombrado
+ label_reply_plural: Respuestas
+ label_report: Informe
+ label_report_plural: Informes
+ label_reported_issues: Peticiones registradas por mí
+ label_repository: Repositorio
+ label_repository_plural: Repositorios
+ label_result_plural: Resultados
+ label_reverse_chronological_order: En orden cronológico inverso
+ label_revision: Revisión
+ label_revision_plural: Revisiones
+ label_roadmap: Planificación
+ label_roadmap_due_in: "Finaliza en {{value}}"
+ label_roadmap_no_issues: No hay peticiones para esta versión
+ label_roadmap_overdue: "{{value}} tarde"
+ label_role: Perfil
+ label_role_and_permissions: Perfiles y permisos
+ label_role_new: Nuevo perfil
+ label_role_plural: Perfiles
+ label_scm: SCM
+ label_search: Búsqueda
+ label_search_titles_only: Buscar sólo en títulos
+ label_send_information: Enviar información de la cuenta al usuario
+ label_send_test_email: Enviar un correo de prueba
+ label_settings: Configuración
+ label_show_completed_versions: Muestra las versiones terminadas
+ label_sort_by: "Ordenar por {{value}}"
+ label_sort_higher: Subir
+ label_sort_highest: Primero
+ label_sort_lower: Bajar
+ label_sort_lowest: Último
+ label_spent_time: Tiempo dedicado
+ label_start_to_end: principio a fin
+ label_start_to_start: principio a principio
+ label_statistics: Estadísticas
+ label_stay_logged_in: Recordar conexión
+ label_string: Texto
+ label_subproject_plural: Proyectos secundarios
+ label_text: Texto largo
+ label_theme: Tema
+ label_this_month: este mes
+ label_this_week: esta semana
+ label_this_year: este año
+ label_time_tracking: Control de tiempo
+ label_today: hoy
+ label_topic_plural: Temas
+ label_total: Total
+ label_tracker: Tipo
+ label_tracker_new: Nuevo tipo
+ label_tracker_plural: Tipos de peticiones
+ label_updated_time: "Actualizado hace {{value}}"
+ label_updated_time_by: "Actualizado por {{author}} hace {{age}}"
+ label_used_by: Utilizado por
+ label_user: Usuario
+ label_user_activity: "Actividad de {{value}}"
+ label_user_mail_no_self_notified: "No quiero ser avisado de cambios hechos por mí"
+ label_user_mail_option_all: "Para cualquier evento en todos mis proyectos"
+ label_user_mail_option_none: "Sólo para elementos monitorizados o relacionados conmigo"
+ label_user_mail_option_selected: "Para cualquier evento de los proyectos seleccionados..."
+ label_user_new: Nuevo usuario
+ label_user_plural: Usuarios
+ label_version: Versión
+ label_version_new: Nueva versión
+ label_version_plural: Versiones
+ label_view_diff: Ver diferencias
+ label_view_revisions: Ver las revisiones
+ label_watched_issues: Peticiones monitorizadas
+ label_week: Semana
+ label_wiki: Wiki
+ label_wiki_edit: Wiki edicción
+ label_wiki_edit_plural: Wiki edicciones
+ label_wiki_page: Wiki página
+ label_wiki_page_plural: Wiki páginas
+ label_workflow: Flujo de trabajo
+ label_year: Año
+ label_yesterday: ayer
+ mail_body_account_activation_request: "Se ha inscrito un nuevo usuario ({{value}}). La cuenta está pendiende de aprobación:"
+ mail_body_account_information: Información sobre su cuenta
+ mail_body_account_information_external: "Puede usar su cuenta {{value}} para conectarse."
+ mail_body_lost_password: 'Para cambiar su contraseña, haga clic en el siguiente enlace:'
+ mail_body_register: 'Para activar su cuenta, haga clic en el siguiente enlace:'
+ mail_body_reminder: "{{count}} peticion(es) asignadas a tí finalizan en los próximos {{days}} días:"
+ mail_subject_account_activation_request: "Petición de activación de cuenta {{value}}"
+ mail_subject_lost_password: "Tu contraseña del {{value}}"
+ mail_subject_register: "Activación de la cuenta del {{value}}"
+ mail_subject_reminder: "{{count}} peticion(es) finalizan en los próximos días"
+ notice_account_activated: Su cuenta ha sido activada. Ya puede conectarse.
+ notice_account_invalid_creditentials: Usuario o contraseña inválido.
+ notice_account_lost_email_sent: Se le ha enviado un correo con instrucciones para elegir una nueva contraseña.
+ notice_account_password_updated: Contraseña modificada correctamente.
+ notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte del administrador."
+ notice_account_register_done: Cuenta creada correctamente. Para activarla, haga clic sobre el enlace que le ha sido enviado por correo.
+ notice_account_unknown_email: Usuario desconocido.
+ notice_account_updated: Cuenta actualizada correctamente.
+ notice_account_wrong_password: Contraseña incorrecta.
+ notice_can_t_change_password: Esta cuenta utiliza una fuente de autenticación externa. No es posible cambiar la contraseña.
+ notice_default_data_loaded: Configuración por defecto cargada correctamente.
+ notice_email_error: "Ha ocurrido un error mientras enviando el correo ({{value}})"
+ notice_email_sent: "Se ha enviado un correo a {{value}}"
+ notice_failed_to_save_issues: "Imposible grabar %s peticion(es) en {{count}} seleccionado: {{ids}}."
+ notice_feeds_access_key_reseted: Su clave de acceso para RSS ha sido reiniciada.
+ notice_file_not_found: La página a la que intenta acceder no existe.
+ notice_locking_conflict: Los datos han sido modificados por otro usuario.
+ notice_no_issue_selected: "Ninguna petición seleccionada. Por favor, compruebe la petición que quiere modificar"
+ notice_not_authorized: No tiene autorización para acceder a esta página.
+ notice_successful_connection: Conexión correcta.
+ notice_successful_create: Creación correcta.
+ notice_successful_delete: Borrado correcto.
+ notice_successful_update: Modificación correcta.
+ notice_unable_delete_version: No se puede borrar la versión
+ permission_add_issue_notes: Añadir notas
+ permission_add_issue_watchers: Añadir seguidores
+ permission_add_issues: Añadir peticiones
+ permission_add_messages: Enviar mensajes
+ permission_browse_repository: Hojear repositiorio
+ permission_comment_news: Comentar noticias
+ permission_commit_access: Acceso de escritura
+ permission_delete_issues: Borrar peticiones
+ permission_delete_messages: Borrar mensajes
+ permission_delete_own_messages: Borrar mensajes propios
+ permission_delete_wiki_pages: Borrar páginas wiki
+ permission_delete_wiki_pages_attachments: Borrar ficheros
+ permission_edit_issue_notes: Modificar notas
+ permission_edit_issues: Modificar peticiones
+ permission_edit_messages: Modificar mensajes
+ permission_edit_own_issue_notes: Modificar notas propias
+ permission_edit_own_messages: Editar mensajes propios
+ permission_edit_own_time_entries: Modificar tiempos dedicados propios
+ permission_edit_project: Modificar proyecto
+ permission_edit_time_entries: Modificar tiempos dedicados
+ permission_edit_wiki_pages: Modificar páginas wiki
+ permission_log_time: Anotar tiempo dedicado
+ permission_manage_boards: Administrar foros
+ permission_manage_categories: Administrar categorías de peticiones
+ permission_manage_documents: Administrar documentos
+ permission_manage_files: Administrar ficheros
+ permission_manage_issue_relations: Administrar relación con otras peticiones
+ permission_manage_members: Administrar miembros
+ permission_manage_news: Administrar noticias
+ permission_manage_public_queries: Administrar consultas públicas
+ permission_manage_repository: Administrar repositorio
+ permission_manage_versions: Administrar versiones
+ permission_manage_wiki: Administrar wiki
+ permission_move_issues: Mover peticiones
+ permission_protect_wiki_pages: Proteger páginas wiki
+ permission_rename_wiki_pages: Renombrar páginas wiki
+ permission_save_queries: Grabar consultas
+ permission_select_project_modules: Seleccionar módulos del proyecto
+ permission_view_calendar: Ver calendario
+ permission_view_changesets: Ver cambios
+ permission_view_documents: Ver documentos
+ permission_view_files: Ver ficheros
+ permission_view_gantt: Ver diagrama de Gantt
+ permission_view_issue_watchers: Ver lista de seguidores
+ permission_view_messages: Ver mensajes
+ permission_view_time_entries: Ver tiempo dedicado
+ permission_view_wiki_edits: Ver histórico del wiki
+ permission_view_wiki_pages: Ver wiki
+ project_module_boards: Foros
+ project_module_documents: Documentos
+ project_module_files: Ficheros
+ project_module_issue_tracking: Peticiones
+ project_module_news: Noticias
+ project_module_repository: Repositorio
+ project_module_time_tracking: Control de tiempo
+ project_module_wiki: Wiki
+ setting_activity_days_default: Días a mostrar en la actividad de proyecto
+ setting_app_subtitle: Subtítulo de la aplicación
+ setting_app_title: Título de la aplicación
+ setting_attachment_max_size: Tamaño máximo del fichero
+ setting_autofetch_changesets: Autorellenar los commits del repositorio
+ setting_autologin: Conexión automática
+ setting_bcc_recipients: Ocultar las copias de carbón (bcc)
+ setting_commit_fix_keywords: Palabras clave para la corrección
+ setting_commit_logs_encoding: Codificación de los mensajes de commit
+ setting_commit_ref_keywords: Palabras clave para la referencia
+ setting_cross_project_issue_relations: Permitir relacionar peticiones de distintos proyectos
+ setting_date_format: Formato de fecha
+ setting_default_language: Idioma por defecto
+ setting_default_projects_public: Los proyectos nuevos son públicos por defecto
+ setting_diff_max_lines_displayed: Número máximo de diferencias mostradas
+ setting_display_subprojects_issues: Mostrar por defecto peticiones de proy. secundarios en el principal
+ setting_emails_footer: Pie de mensajes
+ setting_enabled_scm: Activar SCM
+ setting_feeds_limit: Límite de contenido para sindicación
+ setting_gravatar_enabled: Usar iconos de usuario (Gravatar)
+ setting_host_name: Nombre y ruta del servidor
+ setting_issue_list_default_columns: Columnas por defecto para la lista de peticiones
+ setting_issues_export_limit: Límite de exportación de peticiones
+ setting_login_required: Se requiere identificación
+ setting_mail_from: Correo desde el que enviar mensajes
+ setting_mail_handler_api_enabled: Activar SW para mensajes entrantes
+ setting_mail_handler_api_key: Clave de la API
+ setting_per_page_options: Objetos por página
+ setting_plain_text_mail: sólo texto plano (no HTML)
+ setting_protocol: Protocolo
+ setting_repositories_encodings: Codificaciones del repositorio
+ setting_self_registration: Registro permitido
+ setting_sequential_project_identifiers: Generar identificadores de proyecto
+ setting_sys_api_enabled: Habilitar SW para la gestión del repositorio
+ setting_text_formatting: Formato de texto
+ setting_time_format: Formato de hora
+ setting_user_format: Formato de nombre de usuario
+ setting_welcome_text: Texto de bienvenida
+ setting_wiki_compression: Compresión del historial del Wiki
+ status_active: activo
+ status_locked: bloqueado
+ status_registered: registrado
+ text_are_you_sure: ¿Está seguro?
+ text_assign_time_entries_to_project: Asignar las horas al proyecto
+ text_caracters_maximum: "{{count}} caracteres como máximo."
+ text_caracters_minimum: "{{count}} caracteres como mínimo"
+ text_comma_separated: Múltiples valores permitidos (separados por coma).
+ text_default_administrator_account_changed: Cuenta de administrador por defecto modificada
+ text_destroy_time_entries: Borrar las horas
+ text_destroy_time_entries_question: Existen {{hours}} horas asignadas a la petición que quiere borrar. ¿Qué quiere hacer ?
+ text_diff_truncated: '... Diferencia truncada por exceder el máximo tamaño visualizable.'
+ text_email_delivery_not_configured: "El envío de correos no está configurado, y las notificaciones se han desactivado. \n Configure el servidor de SMTP en config/email.yml y reinicie la aplicación para activar los cambios."
+ text_enumeration_category_reassign_to: 'Reasignar al siguiente valor:'
+ text_enumeration_destroy_question: "{{count}} objetos con este valor asignado."
+ text_file_repository_writable: Se puede escribir en el repositorio
+ text_issue_added: "Petición {{id}} añadida por {{author}}."
+ text_issue_category_destroy_assignments: Dejar las peticiones sin categoría
+ text_issue_category_destroy_question: "Algunas peticiones ({{count}}) están asignadas a esta categoría. ¿Qué desea hacer?"
+ text_issue_category_reassign_to: Reasignar las peticiones a la categoría
+ text_issue_updated: "La petición {{id}} ha sido actualizada por {{author}}."
+ text_issues_destroy_confirmation: '¿Seguro que quiere borrar las peticiones seleccionadas?'
+ text_issues_ref_in_commit_messages: Referencia y petición de corrección en los mensajes
+ text_length_between: "Longitud entre {{min}} y {{max}} caracteres."
+ text_load_default_configuration: Cargar la configuración por defecto
+ text_min_max_length_info: 0 para ninguna restricción
+ text_no_configuration_data: "Todavía no se han configurado perfiles, ni tipos, estados y flujo de trabajo asociado a peticiones. Se recomiendo encarecidamente cargar la configuración por defecto. Una vez cargada, podrá modificarla."
+ text_project_destroy_confirmation: ¿Estás seguro de querer eliminar el proyecto?
+ text_project_identifier_info: 'Letras minúsculas (a-z), números y signos de puntuación permitidos.<br />Una vez guardado, el identificador no puede modificarse.'
+ text_reassign_time_entries: 'Reasignar las horas a esta petición:'
+ text_regexp_info: ej. ^[A-Z0-9]+$
+ text_repository_usernames_mapping: "Establezca la correspondencia entre los usuarios de Redmine y los presentes en el log del repositorio.\nLos usuarios con el mismo nombre o correo en Redmine y en el repositorio serán asociados automáticamente."
+ text_rmagick_available: RMagick disponible (opcional)
+ text_select_mail_notifications: Seleccionar los eventos a notificar
+ text_select_project_modules: 'Seleccione los módulos a activar para este proyecto:'
+ text_status_changed_by_changeset: "Aplicado en los cambios {{value}}"
+ text_subprojects_destroy_warning: "Los proyectos secundarios: {{value}} también se eliminarán"
+ text_tip_task_begin_day: tarea que comienza este día
+ text_tip_task_begin_end_day: tarea que comienza y termina este día
+ text_tip_task_end_day: tarea que termina este día
+ text_tracker_no_workflow: No hay ningún flujo de trabajo definido para este tipo de petición
+ text_unallowed_characters: Caracteres no permitidos
+ text_user_mail_option: "De los proyectos no seleccionados, sólo recibirá notificaciones sobre elementos monitorizados o elementos en los que esté involucrado (por ejemplo, peticiones de las que usted sea autor o asignadas a usted)."
+ text_user_wrote: "{{value}} escribió:"
+ text_wiki_destroy_confirmation: ¿Seguro que quiere borrar el wiki y todo su contenido?
+ text_workflow_edit: Seleccionar un flujo de trabajo para actualizar
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "No fue posible guardar {{count}} fichero(s)."
+ button_create_and_continue: Crear y continuar
+ text_custom_field_possible_values_info: 'Una línea para cada valor'
+ label_display: Mostrar
+ field_editable: Editable
+ setting_repository_log_display_limit: Número máximo de revisiones mostradas en el fichero de trazas
+ setting_file_max_size_displayed: Tamaño máximo de ficheros de texto a mostrar
+ field_watcher: Seguidor
+ setting_openid: Permitir autenticación y registro con OpenID
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: o acceder mediante OpenID
+ field_content: Contenido
+ label_descending: Descendiente
+ label_sort: Ordenar
+ label_ascending: Ascendente
+ label_date_from_to: Desde {{start}} a {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Esta página tiene {{descendants}} página(s) hija(s) y descendiente(s). ¿Qué desea hacer?
+ text_wiki_page_reassign_children: Reasignar páginas hijas a esta página
+ text_wiki_page_nullify_children: Dejar páginas hijas como páginas raíz
+ text_wiki_page_destroy_children: Eliminar páginas hijas y todos sus descendientes
+ setting_password_min_length: Tamaño mínimo de contraseña
+ field_group_by: Agrupar resultados por
+ mail_subject_wiki_content_updated: "La página wiki '{{page}}' ha sido actualizada"
+ label_wiki_content_added: Página wiki añadida
+ mail_subject_wiki_content_added: "La página wiki '{{page}}' ha sido añadida"
+ mail_body_wiki_content_added: La página wiki '{{page}}' ha sido añadida por {{author}}.
+ label_wiki_content_updated: Página wiki actualizada
+ mail_body_wiki_content_updated: La página wiki '{{page}}' ha sido actualizada por {{author}}.
+ permission_add_project: Crear proyecto
+ setting_new_project_user_role_id: Permiso asignado a un usuario no-administrador para crear proyectos
+ label_view_all_revisions: Visualizar todas las revisiones
+ label_tag: Etiqueta
+ label_branch: Rama
+ error_no_tracker_in_project: No existe un tipo de petición asociado a este proyecto. Verifique la configuración del proyecto, por favor.
+ error_no_default_issue_status: No se ha definido un estado por defecto de petición. Verifique su configuración (Vaya a "Administración -> Estados de las peticiones").
+ text_journal_changed: "{{label}} cambiada {{old}} por {{new}}"
+ text_journal_set_to: "{{label}} establecida a {{value}}"
+ text_journal_deleted: "{{label}} eliminado ({{old}})"
+ label_group_plural: Grupos
+ label_group: Grupo
+ label_group_new: Nuevo grupo
+ label_time_entry_plural: Tiempo dedicado
+ 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
--- /dev/null
+# Finnish translations for Ruby on Rails
+# by Marko Seppä (marko.seppa@gmail.com)
+
+fi:
+ date:
+ formats:
+ default: "%e. %Bta %Y"
+ long: "%A%e. %Bta %Y"
+ short: "%e.%m.%Y"
+
+ day_names: [Sunnuntai, Maanantai, Tiistai, Keskiviikko, Torstai, Perjantai, Lauantai]
+ abbr_day_names: [Su, Ma, Ti, Ke, To, Pe, La]
+ month_names: [~, Tammikuu, Helmikuu, Maaliskuu, Huhtikuu, Toukokuu, Kesäkuu, Heinäkuu, Elokuu, Syyskuu, Lokakuu, Marraskuu, Joulukuu]
+ abbr_month_names: [~, Tammi, Helmi, Maalis, Huhti, Touko, Kesä, Heinä, Elo, Syys, Loka, Marras, Joulu]
+ order: [:day, :month, :year]
+
+ time:
+ formats:
+ default: "%a, %e. %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%e. %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "aamupäivä"
+ pm: "iltapäivä"
+
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: " ja "
+ last_word_connector: " ja "
+
+
+
+ number:
+ format:
+ separator: ","
+ delimiter: "."
+ precision: 3
+
+ currency:
+ format:
+ format: "%n %u"
+ unit: "€"
+ separator: ","
+ delimiter: "."
+ precision: 2
+
+ percentage:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+
+ precision:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Tavua"
+ other: "Tavua"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "puoli minuuttia"
+ less_than_x_seconds:
+ one: "aiemmin kuin sekunti"
+ other: "aiemmin kuin {{count}} sekuntia"
+ x_seconds:
+ one: "sekunti"
+ other: "{{count}} sekuntia"
+ less_than_x_minutes:
+ one: "aiemmin kuin minuutti"
+ other: "aiemmin kuin {{count}} minuuttia"
+ x_minutes:
+ one: "minuutti"
+ other: "{{count}} minuuttia"
+ about_x_hours:
+ one: "noin tunti"
+ other: "noin {{count}} tuntia"
+ x_days:
+ one: "päivä"
+ other: "{{count}} päivää"
+ about_x_months:
+ one: "noin kuukausi"
+ other: "noin {{count}} kuukautta"
+ x_months:
+ one: "kuukausi"
+ other: "{{count}} kuukautta"
+ about_x_years:
+ one: "vuosi"
+ other: "noin {{count}} vuotta"
+ over_x_years:
+ one: "yli vuosi"
+ other: "yli {{count}} vuotta"
+ prompts:
+ year: "Vuosi"
+ month: "Kuukausi"
+ day: "Päivä"
+ hour: "Tunti"
+ minute: "Minuutti"
+ second: "Sekuntia"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "1 virhe esti tämän {{model}} mallinteen tallentamisen"
+ other: "{{count}} virhettä esti tämän {{model}} mallinteen tallentamisen"
+ body: "Seuraavat kentät aiheuttivat ongelmia:"
+ messages:
+ inclusion: "ei löydy listauksesta"
+ exclusion: "on jo varattu"
+ invalid: "on kelvoton"
+ confirmation: "ei vastaa varmennusta"
+ accepted: "täytyy olla hyväksytty"
+ empty: "ei voi olla tyhjä"
+ blank: "ei voi olla sisällötön"
+ too_long: "on liian pitkä (maksimi on {{count}} merkkiä)"
+ too_short: "on liian lyhyt (minimi on {{count}} merkkiä)"
+ wrong_length: "on väärän pituinen (täytyy olla täsmälleen {{count}} merkkiä)"
+ taken: "on jo käytössä"
+ not_a_number: "ei ole numero"
+ greater_than: "täytyy olla suurempi kuin {{count}}"
+ greater_than_or_equal_to: "täytyy olla suurempi tai yhtä suuri kuin{{count}}"
+ equal_to: "täytyy olla yhtä suuri kuin {{count}}"
+ less_than: "täytyy olla pienempi kuin {{count}}"
+ less_than_or_equal_to: "täytyy olla pienempi tai yhtä suuri kuin {{count}}"
+ odd: "täytyy olla pariton"
+ even: "täytyy olla parillinen"
+ greater_than_start_date: "tulee olla aloituspäivän jälkeinen"
+ not_same_project: "ei kuulu samaan projektiin"
+ circular_dependency: "Tämä suhde loisi kehän."
+
+
+ actionview_instancetag_blank_option: Valitse, ole hyvä
+
+ general_text_No: 'Ei'
+ general_text_Yes: 'Kyllä'
+ general_text_no: 'ei'
+ general_text_yes: 'kyllä'
+ general_lang_name: 'Finnish (Suomi)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-15
+ general_pdf_encoding: ISO-8859-15
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Tilin päivitys onnistui.
+ notice_account_invalid_creditentials: Virheellinen käyttäjätunnus tai salasana
+ notice_account_password_updated: Salasanan päivitys onnistui.
+ notice_account_wrong_password: Väärä salasana
+ notice_account_register_done: Tilin luonti onnistui. Aktivoidaksesi tilin seuraa linkkiä joka välitettiin sähköpostiisi.
+ notice_account_unknown_email: Tuntematon käyttäjä.
+ notice_can_t_change_password: Tämä tili käyttää ulkoista tunnistautumisjärjestelmää. Salasanaa ei voi muuttaa.
+ notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje kuinka vaihdat salasanasi.
+ notice_account_activated: Tilisi on nyt aktivoitu, voit kirjautua sisälle.
+ notice_successful_create: Luonti onnistui.
+ notice_successful_update: Päivitys onnistui.
+ notice_successful_delete: Poisto onnistui.
+ notice_successful_connection: Yhteyden muodostus onnistui.
+ notice_file_not_found: Hakemaasi sivua ei löytynyt tai se on poistettu.
+ notice_locking_conflict: Toinen käyttäjä on päivittänyt tiedot.
+ notice_not_authorized: Sinulla ei ole oikeutta näyttää tätä sivua.
+ notice_email_sent: "Sähköposti on lähetty osoitteeseen {{value}}"
+ notice_email_error: "Sähköpostilähetyksessä tapahtui virhe ({{value}})"
+ notice_feeds_access_key_reseted: RSS salasana on nollaantunut.
+ notice_failed_to_save_issues: "{{count}} Tapahtum(an/ien) tallennus epäonnistui {{total}} valitut: {{ids}}."
+ notice_no_issue_selected: "Tapahtumia ei ole valittu! Valitse tapahtumat joita haluat muokata."
+ notice_account_pending: "Tilisi on luotu ja odottaa ylläpitäjän hyväksyntää."
+ notice_default_data_loaded: Vakioasetusten palautus onnistui.
+
+ error_can_t_load_default_data: "Vakioasetuksia ei voitu ladata: {{value}}"
+ error_scm_not_found: "Syötettä ja/tai versiota ei löydy tietovarastosta."
+ error_scm_command_failed: "Tietovarastoon pääsyssä tapahtui virhe: {{value}}"
+
+ mail_subject_lost_password: "Sinun {{value}} salasanasi"
+ mail_body_lost_password: 'Vaihtaaksesi salasanasi, napsauta seuraavaa linkkiä:'
+ mail_subject_register: "{{value}} tilin aktivointi"
+ mail_body_register: 'Aktivoidaksesi tilisi, napsauta seuraavaa linkkiä:'
+ mail_body_account_information_external: "Voit nyt käyttää {{value}} tiliäsi kirjautuaksesi järjestelmään."
+ mail_body_account_information: Sinun tilin tiedot
+ mail_subject_account_activation_request: "{{value}} tilin aktivointi pyyntö"
+ mail_body_account_activation_request: "Uusi käyttäjä ({{value}}) on rekisteröitynyt. Hänen tili odottaa hyväksyntääsi:"
+
+ gui_validation_error: 1 virhe
+ gui_validation_error_plural: "{{count}} virhettä"
+
+ field_name: Nimi
+ field_description: Kuvaus
+ field_summary: Yhteenveto
+ field_is_required: Vaaditaan
+ field_firstname: Etunimi
+ field_lastname: Sukunimi
+ field_mail: Sähköposti
+ field_filename: Tiedosto
+ field_filesize: Koko
+ field_downloads: Latausta
+ field_author: Tekijä
+ field_created_on: Luotu
+ field_updated_on: Päivitetty
+ field_field_format: Muoto
+ field_is_for_all: Kaikille projekteille
+ field_possible_values: Mahdolliset arvot
+ field_regexp: Säännöllinen lauseke (reg exp)
+ field_min_length: Minimipituus
+ field_max_length: Maksimipituus
+ field_value: Arvo
+ field_category: Luokka
+ field_title: Otsikko
+ field_project: Projekti
+ field_issue: Tapahtuma
+ field_status: Tila
+ field_notes: Muistiinpanot
+ field_is_closed: Tapahtuma suljettu
+ field_is_default: Vakioarvo
+ field_tracker: Tapahtuma
+ field_subject: Aihe
+ field_due_date: Määräaika
+ field_assigned_to: Nimetty
+ field_priority: Prioriteetti
+ field_fixed_version: Kohdeversio
+ field_user: Käyttäjä
+ field_role: Rooli
+ field_homepage: Kotisivu
+ field_is_public: Julkinen
+ field_parent: Aliprojekti
+ field_is_in_chlog: Tapahtumat näytetään muutoslokissa
+ field_is_in_roadmap: Tapahtumat näytetään roadmap näkymässä
+ field_login: Kirjautuminen
+ field_mail_notification: Sähköposti muistutukset
+ field_admin: Ylläpitäjä
+ field_last_login_on: Viimeinen yhteys
+ field_language: Kieli
+ field_effective_date: Päivä
+ field_password: Salasana
+ field_new_password: Uusi salasana
+ field_password_confirmation: Vahvistus
+ field_version: Versio
+ field_type: Tyyppi
+ field_host: Verkko-osoite
+ field_port: Portti
+ field_account: Tili
+ field_base_dn: Base DN
+ field_attr_login: Kirjautumismääre
+ field_attr_firstname: Etuminenmääre
+ field_attr_lastname: Sukunimenmääre
+ field_attr_mail: Sähköpostinmääre
+ field_onthefly: Automaattinen käyttäjien luonti
+ field_start_date: Alku
+ field_done_ratio: % Tehty
+ field_auth_source: Varmennusmuoto
+ field_hide_mail: Piiloita sähköpostiosoitteeni
+ field_comments: Kommentti
+ field_url: URL
+ field_start_page: Aloitussivu
+ field_subproject: Aliprojekti
+ field_hours: Tuntia
+ field_activity: Historia
+ field_spent_on: Päivä
+ field_identifier: Tunniste
+ field_is_filter: Käytetään suodattimena
+ field_issue_to: Liittyvä tapahtuma
+ field_delay: Viive
+ field_assignable: Tapahtumia voidaan nimetä tälle roolille
+ field_redirect_existing_links: Uudelleenohjaa olemassa olevat linkit
+ field_estimated_hours: Arvioitu aika
+ field_column_names: Saraketta
+ field_time_zone: Aikavyöhyke
+ field_searchable: Haettava
+ field_default_value: Vakioarvo
+
+ setting_app_title: Ohjelman otsikko
+ setting_app_subtitle: Ohjelman alaotsikko
+ setting_welcome_text: Tervehdysteksti
+ setting_default_language: Vakiokieli
+ setting_login_required: Pakollinen kirjautuminen
+ setting_self_registration: Itserekisteröinti
+ setting_attachment_max_size: Liitteen maksimikoko
+ setting_issues_export_limit: Tapahtumien vientirajoite
+ setting_mail_from: Lähettäjän sähköpostiosoite
+ setting_bcc_recipients: Vastaanottajat piilokopiona (bcc)
+ setting_host_name: Verkko-osoite
+ setting_text_formatting: Tekstin muotoilu
+ setting_wiki_compression: Wiki historian pakkaus
+ setting_feeds_limit: Syötteen sisällön raja
+ setting_autofetch_changesets: Automaattisten muutosjoukkojen haku
+ setting_sys_api_enabled: Salli WS tietovaraston hallintaan
+ setting_commit_ref_keywords: Viittaavat hakusanat
+ setting_commit_fix_keywords: Korjaavat hakusanat
+ setting_autologin: Automaatinen kirjautuminen
+ setting_date_format: Päivän muoto
+ setting_time_format: Ajan muoto
+ setting_cross_project_issue_relations: Salli projektien väliset tapahtuminen suhteet
+ setting_issue_list_default_columns: Vakiosarakkeiden näyttö tapahtumalistauksessa
+ setting_repositories_encodings: Tietovaraston koodaus
+ setting_emails_footer: Sähköpostin alatunniste
+ setting_protocol: Protokolla
+ setting_per_page_options: Sivun objektien määrän asetukset
+
+ label_user: Käyttäjä
+ label_user_plural: Käyttäjät
+ label_user_new: Uusi käyttäjä
+ label_project: Projekti
+ label_project_new: Uusi projekti
+ label_project_plural: Projektit
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Kaikki projektit
+ label_project_latest: Uusimmat projektit
+ label_issue: Tapahtuma
+ label_issue_new: Uusi tapahtuma
+ label_issue_plural: Tapahtumat
+ label_issue_view_all: Näytä kaikki tapahtumat
+ label_issues_by: "Tapahtumat {{value}}"
+ label_document: Dokumentti
+ label_document_new: Uusi dokumentti
+ label_document_plural: Dokumentit
+ label_role: Rooli
+ label_role_plural: Roolit
+ label_role_new: Uusi rooli
+ label_role_and_permissions: Roolit ja oikeudet
+ label_member: Jäsen
+ label_member_new: Uusi jäsen
+ label_member_plural: Jäsenet
+ label_tracker: Tapahtuma
+ label_tracker_plural: Tapahtumat
+ label_tracker_new: Uusi tapahtuma
+ label_workflow: Työnkulku
+ label_issue_status: Tapahtuman tila
+ label_issue_status_plural: Tapahtumien tilat
+ label_issue_status_new: Uusi tila
+ label_issue_category: Tapahtumaluokka
+ label_issue_category_plural: Tapahtumaluokat
+ label_issue_category_new: Uusi luokka
+ label_custom_field: Räätälöity kenttä
+ label_custom_field_plural: Räätälöidyt kentät
+ label_custom_field_new: Uusi räätälöity kenttä
+ label_enumerations: Lista
+ label_enumeration_new: Uusi arvo
+ label_information: Tieto
+ label_information_plural: Tiedot
+ label_please_login: Kirjaudu ole hyvä
+ label_register: Rekisteröidy
+ label_password_lost: Hukattu salasana
+ label_home: Koti
+ label_my_page: Omasivu
+ label_my_account: Oma tili
+ label_my_projects: Omat projektit
+ label_administration: Ylläpito
+ label_login: Kirjaudu sisään
+ label_logout: Kirjaudu ulos
+ label_help: Ohjeet
+ label_reported_issues: Raportoidut tapahtumat
+ label_assigned_to_me_issues: Minulle nimetyt tapahtumat
+ label_last_login: Viimeinen yhteys
+ label_registered_on: Rekisteröity
+ label_activity: Historia
+ label_new: Uusi
+ label_logged_as: Kirjauduttu nimellä
+ label_environment: Ympäristö
+ label_authentication: Varmennus
+ label_auth_source: Varmennustapa
+ label_auth_source_new: Uusi varmennustapa
+ label_auth_source_plural: Varmennustavat
+ label_subproject_plural: Aliprojektit
+ label_min_max_length: Min - Max pituudet
+ label_list: Lista
+ label_date: Päivä
+ label_integer: Kokonaisluku
+ label_float: Liukuluku
+ label_boolean: Totuusarvomuuttuja
+ label_string: Merkkijono
+ label_text: Pitkä merkkijono
+ label_attribute: Määre
+ label_attribute_plural: Määreet
+ label_download: "{{count}} Lataus"
+ label_download_plural: "{{count}} Lataukset"
+ label_no_data: Ei tietoa näytettäväksi
+ label_change_status: Muutos tila
+ label_history: Historia
+ label_attachment: Tiedosto
+ label_attachment_new: Uusi tiedosto
+ label_attachment_delete: Poista tiedosto
+ label_attachment_plural: Tiedostot
+ label_report: Raportti
+ label_report_plural: Raportit
+ label_news: Uutinen
+ label_news_new: Lisää uutinen
+ label_news_plural: Uutiset
+ label_news_latest: Viimeisimmät uutiset
+ label_news_view_all: Näytä kaikki uutiset
+ label_change_log: Muutosloki
+ label_settings: Asetukset
+ label_overview: Yleiskatsaus
+ label_version: Versio
+ label_version_new: Uusi versio
+ label_version_plural: Versiot
+ label_confirmation: Vahvistus
+ label_export_to: Vie
+ label_read: Lukee...
+ label_public_projects: Julkiset projektit
+ label_open_issues: avoin, yhteensä
+ label_open_issues_plural: avointa, yhteensä
+ label_closed_issues: suljettu
+ label_closed_issues_plural: suljettua
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Yhteensä
+ label_permissions: Oikeudet
+ label_current_status: Nykyinen tila
+ label_new_statuses_allowed: Uudet tilat sallittu
+ label_all: kaikki
+ label_none: ei mitään
+ label_nobody: ei kukaan
+ label_next: Seuraava
+ label_previous: Edellinen
+ label_used_by: Käytetty
+ label_details: Yksityiskohdat
+ label_add_note: Lisää muistiinpano
+ label_per_page: Per sivu
+ label_calendar: Kalenteri
+ label_months_from: kuukauden päässä
+ label_gantt: Gantt
+ label_internal: Sisäinen
+ label_last_changes: "viimeiset {{count}} muutokset"
+ label_change_view_all: Näytä kaikki muutokset
+ label_personalize_page: Personoi tämä sivu
+ label_comment: Kommentti
+ label_comment_plural: Kommentit
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Lisää kommentti
+ label_comment_added: Kommentti lisätty
+ label_comment_delete: Poista kommentti
+ label_query: Räätälöity haku
+ label_query_plural: Räätälöidyt haut
+ label_query_new: Uusi haku
+ label_filter_add: Lisää suodatin
+ label_filter_plural: Suodattimet
+ label_equals: sama kuin
+ label_not_equals: eri kuin
+ label_in_less_than: pienempi kuin
+ label_in_more_than: suurempi kuin
+ label_today: tänään
+ label_this_week: tällä viikolla
+ label_less_than_ago: vähemmän kuin päivää sitten
+ label_more_than_ago: enemän kuin päivää sitten
+ label_ago: päiviä sitten
+ label_contains: sisältää
+ label_not_contains: ei sisällä
+ label_day_plural: päivää
+ label_repository: Tietovarasto
+ label_repository_plural: Tietovarastot
+ label_browse: Selaus
+ label_modification: "{{count}} muutos"
+ label_modification_plural: "{{count}} muutettu"
+ label_revision: Versio
+ label_revision_plural: Versiot
+ label_added: lisätty
+ label_modified: muokattu
+ label_deleted: poistettu
+ label_latest_revision: Viimeisin versio
+ label_latest_revision_plural: Viimeisimmät versiot
+ label_view_revisions: Näytä versiot
+ label_max_size: Suurin koko
+ label_sort_highest: Siirrä ylimmäiseksi
+ label_sort_higher: Siirrä ylös
+ label_sort_lower: Siirrä alas
+ label_sort_lowest: Siirrä alimmaiseksi
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Määräaika {{value}}"
+ label_roadmap_overdue: "{{value}} myöhässä"
+ label_roadmap_no_issues: Ei tapahtumia tälle versiolle
+ label_search: Haku
+ label_result_plural: Tulokset
+ label_all_words: kaikki sanat
+ label_wiki: Wiki
+ label_wiki_edit: Wiki muokkaus
+ label_wiki_edit_plural: Wiki muokkaukset
+ label_wiki_page: Wiki sivu
+ label_wiki_page_plural: Wiki sivut
+ label_index_by_title: Hakemisto otsikoittain
+ label_index_by_date: Hakemisto päivittäin
+ label_current_version: Nykyinen versio
+ label_preview: Esikatselu
+ label_feed_plural: Syötteet
+ label_changes_details: Kaikkien muutosten yksityiskohdat
+ label_issue_tracking: Tapahtumien seuranta
+ label_spent_time: Käytetty aika
+ label_f_hour: "{{value}} tunti"
+ label_f_hour_plural: "{{value}} tuntia"
+ label_time_tracking: Ajan seuranta
+ label_change_plural: Muutokset
+ label_statistics: Tilastot
+ label_commits_per_month: Tapahtumaa per kuukausi
+ label_commits_per_author: Tapahtumaa per tekijä
+ label_view_diff: Näytä erot
+ label_diff_inline: sisällössä
+ label_diff_side_by_side: vierekkäin
+ label_options: Valinnat
+ label_copy_workflow_from: Kopioi työnkulku
+ label_permissions_report: Oikeuksien raportti
+ label_watched_issues: Seurattavat tapahtumat
+ label_related_issues: Liittyvät tapahtumat
+ label_applied_status: Lisätty tila
+ label_loading: Lataa...
+ label_relation_new: Uusi suhde
+ label_relation_delete: Poista suhde
+ label_relates_to: liittyy
+ label_duplicates: kopio
+ label_blocks: estää
+ label_blocked_by: estetty
+ label_precedes: edeltää
+ label_follows: seuraa
+ label_end_to_start: lopusta alkuun
+ label_end_to_end: lopusta loppuun
+ label_start_to_start: alusta alkuun
+ label_start_to_end: alusta loppuun
+ label_stay_logged_in: Pysy kirjautuneena
+ label_disabled: poistettu käytöstä
+ label_show_completed_versions: Näytä valmiit versiot
+ label_me: minä
+ label_board: Keskustelupalsta
+ label_board_new: Uusi keskustelupalsta
+ label_board_plural: Keskustelupalstat
+ label_topic_plural: Aiheet
+ label_message_plural: Viestit
+ label_message_last: Viimeisin viesti
+ label_message_new: Uusi viesti
+ label_reply_plural: Vastaukset
+ label_send_information: Lähetä tilin tiedot käyttäjälle
+ label_year: Vuosi
+ label_month: Kuukausi
+ label_week: Viikko
+ label_language_based: Pohjautuen käyttäjän kieleen
+ label_sort_by: "Lajittele {{value}}"
+ label_send_test_email: Lähetä testi sähköposti
+ label_feeds_access_key_created_on: "RSS salasana luotiin {{value}} sitten"
+ label_module_plural: Moduulit
+ label_added_time_by: "Lisännyt {{author}} {{age}} sitten"
+ label_updated_time: "Päivitetty {{value}} sitten"
+ label_jump_to_a_project: Siirry projektiin...
+ label_file_plural: Tiedostot
+ label_changeset_plural: Muutosryhmät
+ label_default_columns: Vakiosarakkeet
+ label_no_change_option: (Ei muutosta)
+ label_bulk_edit_selected_issues: Perusmuotoile valitut tapahtumat
+ label_theme: Teema
+ label_default: Vakio
+ label_search_titles_only: Hae vain otsikot
+ label_user_mail_option_all: "Kaikista tapahtumista kaikissa projekteistani"
+ label_user_mail_option_selected: "Kaikista tapahtumista vain valitsemistani projekteista..."
+ label_user_mail_option_none: "Vain tapahtumista joita valvon tai olen mukana"
+ label_user_mail_no_self_notified: "En halua muistutusta muutoksista joita itse teen"
+ label_registration_activation_by_email: tilin aktivointi sähköpostitse
+ label_registration_manual_activation: tilin aktivointi käsin
+ label_registration_automatic_activation: tilin aktivointi automaattisesti
+ label_display_per_page: "Per sivu: {{value}}"
+ label_age: Ikä
+ label_change_properties: Vaihda asetuksia
+ label_general: Yleinen
+
+ button_login: Kirjaudu
+ button_submit: Lähetä
+ button_save: Tallenna
+ button_check_all: Valitse kaikki
+ button_uncheck_all: Poista valinnat
+ button_delete: Poista
+ button_create: Luo
+ button_test: Testaa
+ button_edit: Muokkaa
+ button_add: Lisää
+ button_change: Muuta
+ button_apply: Ota käyttöön
+ button_clear: Tyhjää
+ button_lock: Lukitse
+ button_unlock: Vapauta
+ button_download: Lataa
+ button_list: Lista
+ button_view: Näytä
+ button_move: Siirrä
+ button_back: Takaisin
+ button_cancel: Peruuta
+ button_activate: Aktivoi
+ button_sort: Järjestä
+ button_log_time: Seuraa aikaa
+ button_rollback: Siirry takaisin tähän versioon
+ button_watch: Seuraa
+ button_unwatch: Älä seuraa
+ button_reply: Vastaa
+ button_archive: Arkistoi
+ button_unarchive: Palauta
+ button_reset: Nollaus
+ button_rename: Uudelleen nimeä
+ button_change_password: Vaihda salasana
+ button_copy: Kopioi
+ button_annotate: Lisää selitys
+ button_update: Päivitä
+
+ status_active: aktiivinen
+ status_registered: rekisteröity
+ status_locked: lukittu
+
+ text_select_mail_notifications: Valitse tapahtumat joista tulisi lähettää sähköpostimuistutus.
+ text_regexp_info: esim. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 tarkoittaa, ei rajoitusta
+ text_project_destroy_confirmation: Oletko varma että haluat poistaa tämän projektin ja kaikki siihen kuuluvat tiedot?
+ text_workflow_edit: Valitse rooli ja tapahtuma muokataksesi työnkulkua
+ text_are_you_sure: Oletko varma?
+ text_tip_task_begin_day: tehtävä joka alkaa tänä päivänä
+ text_tip_task_end_day: tehtävä joka loppuu tänä päivänä
+ text_tip_task_begin_end_day: tehtävä joka alkaa ja loppuu tänä päivänä
+ text_project_identifier_info: 'Pienet kirjaimet (a-z), numerot ja viivat ovat sallittu.<br />Tallentamisen jälkeen tunnistetta ei voi muuttaa.'
+ text_caracters_maximum: "{{count}} merkkiä enintään."
+ text_caracters_minimum: "Täytyy olla vähintään {{count}} merkkiä pitkä."
+ text_length_between: "Pituus välillä {{min}} ja {{max}} merkkiä."
+ text_tracker_no_workflow: Työnkulkua ei määritelty tälle tapahtumalle
+ text_unallowed_characters: Kiellettyjä merkkejä
+ text_comma_separated: Useat arvot sallittu (pilkku eroteltuna).
+ text_issues_ref_in_commit_messages: Liitän ja korjaan ongelmia syötetyssä viestissä
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{author}}."
+ text_wiki_destroy_confirmation: Oletko varma että haluat poistaa tämän wiki:n ja kaikki sen sisältämän tiedon?
+ text_issue_category_destroy_question: "Jotkut tapahtumat ({{count}}) ovat nimetty tälle luokalle. Mitä haluat tehdä?"
+ text_issue_category_destroy_assignments: Poista luokan tehtävät
+ text_issue_category_reassign_to: Vaihda tapahtuma tähän luokkaan
+ text_user_mail_option: "Valitsemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)."
+ text_no_configuration_data: "Rooleja, tapahtumien tiloja ja työnkulkua ei vielä olla määritelty.\nOn erittäin suotavaa ladata vakioasetukset. Voit muuttaa sitä latauksen jälkeen."
+ text_load_default_configuration: Lataa vakioasetukset
+
+ default_role_manager: Päälikkö
+ default_role_developper: Kehittäjä
+ default_role_reporter: Tarkastelija
+ default_tracker_bug: Ohjelmointivirhe
+ default_tracker_feature: Ominaisuus
+ default_tracker_support: Tuki
+ default_issue_status_new: Uusi
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Hyväksytty
+ default_issue_status_feedback: Palaute
+ default_issue_status_closed: Suljettu
+ default_issue_status_rejected: Hylätty
+ default_doc_category_user: Käyttäjä dokumentaatio
+ default_doc_category_tech: Tekninen dokumentaatio
+ default_priority_low: Matala
+ default_priority_normal: Normaali
+ default_priority_high: Korkea
+ default_priority_urgent: Kiireellinen
+ default_priority_immediate: Valitön
+ default_activity_design: Suunnittelu
+ default_activity_development: Kehitys
+
+ enumeration_issue_priorities: Tapahtuman tärkeysjärjestys
+ enumeration_doc_categories: Dokumentin luokat
+ enumeration_activities: Historia (ajan seuranta)
+ label_associated_revisions: Liittyvät versiot
+ setting_user_format: Käyttäjien esitysmuoto
+ text_status_changed_by_changeset: "Päivitetty muutosversioon {{value}}."
+ text_issues_destroy_confirmation: 'Oletko varma että haluat poistaa valitut tapahtumat ?'
+ label_more: Lisää
+ label_issue_added: Tapahtuma lisätty
+ label_issue_updated: Tapahtuma päivitetty
+ label_document_added: Dokumentti lisätty
+ label_message_posted: Viesti lisätty
+ label_file_added: Tiedosto lisätty
+ label_scm: SCM
+ text_select_project_modules: 'Valitse modulit jotka haluat käyttöön tähän projektiin:'
+ label_news_added: Uutinen lisätty
+ project_module_boards: Keskustelupalsta
+ project_module_issue_tracking: Tapahtuman seuranta
+ project_module_wiki: Wiki
+ project_module_files: Tiedostot
+ project_module_documents: Dokumentit
+ project_module_repository: Tietovarasto
+ project_module_news: Uutiset
+ project_module_time_tracking: Ajan seuranta
+ text_file_repository_writable: Kirjoitettava tiedostovarasto
+ text_default_administrator_account_changed: Vakio hallinoijan tunnus muutettu
+ text_rmagick_available: RMagick saatavilla (valinnainen)
+ button_configure: Asetukset
+ label_plugins: Lisäosat
+ label_ldap_authentication: LDAP tunnistautuminen
+ label_downloads_abbr: D/L
+ label_add_another_file: Lisää uusi tiedosto
+ label_this_month: tässä kuussa
+ text_destroy_time_entries_question: "{{hours}} tuntia on raportoitu tapahtumasta jonka aiot poistaa. Mitä haluat tehdä ?"
+ label_last_n_days: "viimeiset {{count}} päivää"
+ label_all_time: koko ajalta
+ error_issue_not_found_in_project: 'Tapahtumaa ei löytynyt tai se ei kuulu tähän projektiin'
+ label_this_year: tänä vuonna
+ text_assign_time_entries_to_project: Määritä tunnit projektille
+ label_date_range: Aikaväli
+ label_last_week: viime viikolla
+ label_yesterday: eilen
+ label_optional_description: Lisäkuvaus
+ label_last_month: viime kuussa
+ text_destroy_time_entries: Poista raportoidut tunnit
+ text_reassign_time_entries: 'Siirrä raportoidut tunnit tälle tapahtumalle:'
+ label_chronological_order: Aikajärjestyksessä
+ label_date_to: ''
+ setting_activity_days_default: Päivien esittäminen projektien historiassa
+ label_date_from: ''
+ label_in: ''
+ setting_display_subprojects_issues: Näytä aliprojektien tapahtumat pääprojektissa oletusarvoisesti
+ field_comments_sorting: Näytä kommentit
+ label_reverse_chronological_order: Käänteisessä aikajärjestyksessä
+ label_preferences: Asetukset
+ setting_default_projects_public: Uudet projektit ovat oletuksena julkisia
+ label_overall_activity: Kokonaishistoria
+ error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä."
+ label_planning: Suunnittelu
+ text_subprojects_destroy_warning: "Tämän aliprojekti(t): {{value}} tullaan myös poistamaan."
+ label_and_its_subprojects: "{{value}} ja aliprojektit"
+ mail_body_reminder: "{{count}} sinulle nimettyä tapahtuma(a) erääntyy {{days}} päivä sisään:"
+ mail_subject_reminder: "{{count}} tapahtuma(a) erääntyy lähipäivinä"
+ text_user_wrote: "{{value}} kirjoitti:"
+ label_duplicated_by: kopioinut
+ setting_enabled_scm: Versionhallinta käytettävissä
+ text_enumeration_category_reassign_to: 'Siirrä täksi arvoksi:'
+ text_enumeration_destroy_question: "{{count}} kohdetta on sijoitettu tälle arvolle."
+ label_incoming_emails: Saapuvat sähköpostiviestit
+ label_generate_key: Luo avain
+ setting_mail_handler_api_enabled: Ota käyttöön WS saapuville sähköposteille
+ setting_mail_handler_api_key: API avain
+ text_email_delivery_not_configured: "Sähköpostin jakelu ei ole määritelty ja sähköpostimuistutukset eivät ole käytössä.\nKonfiguroi sähköpostipalvelinasetukset (SMTP) config/email.yml tiedostosta ja uudelleenkäynnistä sovellus jotta asetukset astuvat voimaan."
+ field_parent_title: Aloitussivu
+ label_issue_watchers: Tapahtuman seuraajat
+ button_quote: Vastaa
+ setting_sequential_project_identifiers: Luo peräkkäiset projektien tunnisteet
+ setting_commit_logs_encoding: Tee viestien koodaus
+ notice_unable_delete_version: Version poisto epäonnistui
+ label_renamed: uudelleennimetty
+ label_copied: kopioitu
+ setting_plain_text_mail: vain muotoilematonta tekstiä (ei HTML)
+ permission_view_files: Näytä tiedostot
+ permission_edit_issues: Muokkaa tapahtumia
+ permission_edit_own_time_entries: Muokka omia aikamerkintöjä
+ permission_manage_public_queries: Hallinnoi julkisia hakuja
+ permission_add_issues: Lisää tapahtumia
+ permission_log_time: Lokita käytettyä aikaa
+ permission_view_changesets: Näytä muutosryhmät
+ permission_view_time_entries: Näytä käytetty aika
+ permission_manage_versions: Hallinnoi versioita
+ permission_manage_wiki: Hallinnoi wikiä
+ permission_manage_categories: Hallinnoi tapahtumien luokkia
+ permission_protect_wiki_pages: Suojaa wiki sivut
+ permission_comment_news: Kommentoi uutisia
+ permission_delete_messages: Poista viestit
+ permission_select_project_modules: Valitse projektin modulit
+ permission_manage_documents: Hallinnoi dokumentteja
+ permission_edit_wiki_pages: Muokkaa wiki sivuja
+ permission_add_issue_watchers: Lisää seuraajia
+ permission_view_gantt: Näytä gantt kaavio
+ permission_move_issues: Siirrä tapahtuma
+ permission_manage_issue_relations: Hallinoi tapahtuman suhteita
+ permission_delete_wiki_pages: Poista wiki sivuja
+ permission_manage_boards: Hallinnoi keskustelupalstaa
+ permission_delete_wiki_pages_attachments: Poista liitteitä
+ permission_view_wiki_edits: Näytä wiki historia
+ permission_add_messages: Jätä viesti
+ permission_view_messages: Näytä viestejä
+ permission_manage_files: Hallinnoi tiedostoja
+ permission_edit_issue_notes: Muokkaa muistiinpanoja
+ permission_manage_news: Hallinnoi uutisia
+ permission_view_calendar: Näytä kalenteri
+ permission_manage_members: Hallinnoi jäseniä
+ permission_edit_messages: Muokkaa viestejä
+ permission_delete_issues: Poista tapahtumia
+ permission_view_issue_watchers: Näytä seuraaja lista
+ permission_manage_repository: Hallinnoi tietovarastoa
+ permission_commit_access: Tee pääsyoikeus
+ permission_browse_repository: Selaa tietovarastoa
+ permission_view_documents: Näytä dokumentit
+ permission_edit_project: Muokkaa projektia
+ permission_add_issue_notes: Lisää muistiinpanoja
+ permission_save_queries: Tallenna hakuja
+ permission_view_wiki_pages: Näytä wiki
+ permission_rename_wiki_pages: Uudelleennimeä wiki sivuja
+ permission_edit_time_entries: Muokkaa aika lokeja
+ permission_edit_own_issue_notes: Muokkaa omia muistiinpanoja
+ setting_gravatar_enabled: Käytä Gravatar käyttäjä ikoneita
+ label_example: Esimerkki
+ text_repository_usernames_mapping: "Valitse päivittääksesi Redmine käyttäjä jokaiseen käyttäjään joka löytyy tietovaraston lokista.\nKäyttäjät joilla on sama Redmine ja tietovaraston käyttäjänimi tai sähköpostiosoite, yhdistetään automaattisesti."
+ permission_edit_own_messages: Muokkaa omia viestejä
+ permission_delete_own_messages: Poista omia viestejä
+ label_user_activity: "Käyttäjän {{value}} historia"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# French translations for Ruby on Rails
+# by Christian Lescuyer (christian@flyingcoders.com)
+# contributor: Sebastien Grosjean - ZenCocoon.com
+
+fr:
+ date:
+ formats:
+ default: "%d/%m/%Y"
+ short: "%e %b"
+ long: "%e %B %Y"
+ long_ordinal: "%e %B %Y"
+ only_day: "%e"
+
+ day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
+ abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
+ month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
+ abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%d/%m/%Y %H:%M"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%A %d %B %Y %H:%M:%S %Z"
+ long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
+ only_second: "%S"
+ am: 'am'
+ pm: 'pm'
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "30 secondes"
+ less_than_x_seconds:
+ zero: "moins d'une seconde"
+ one: "moins de 1 seconde"
+ other: "moins de {{count}} secondes"
+ x_seconds:
+ one: "1 seconde"
+ other: "{{count}} secondes"
+ less_than_x_minutes:
+ zero: "moins d'une minute"
+ one: "moins de 1 minute"
+ other: "moins de {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "environ une heure"
+ other: "environ {{count}} heures"
+ x_days:
+ one: "1 jour"
+ other: "{{count}} jours"
+ about_x_months:
+ one: "environ un mois"
+ other: "environ {{count}} mois"
+ x_months:
+ one: "1 mois"
+ other: "{{count}} mois"
+ about_x_years:
+ one: "environ un an"
+ other: "environ {{count}} ans"
+ over_x_years:
+ one: "plus d'un an"
+ other: "plus de {{count}} ans"
+ prompts:
+ year: "Année"
+ month: "Mois"
+ day: "Jour"
+ hour: "Heure"
+ minute: "Minute"
+ second: "Seconde"
+
+ number:
+ format:
+ precision: 3
+ separator: ','
+ delimiter: ' '
+ currency:
+ format:
+ unit: '€'
+ precision: 2
+ format: '%n %u'
+ human:
+ format:
+ precision: 2
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Octet"
+ other: "Octet"
+ kb: "ko"
+ mb: "Mo"
+ gb: "Go"
+ tb: "To"
+
+ support:
+ array:
+ sentence_connector: 'et'
+ skip_last_comma: true
+ word_connector: ", "
+ two_words_connector: " et "
+ last_word_connector: " et "
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Impossible d'enregistrer {{model}}: 1 erreur"
+ other: "Impossible d'enregistrer {{model}}: {{count}} erreurs."
+ body: "Veuillez vérifier les champs suivants :"
+ messages:
+ inclusion: "n'est pas inclus(e) dans la liste"
+ exclusion: "n'est pas disponible"
+ invalid: "n'est pas valide"
+ confirmation: "ne concorde pas avec la confirmation"
+ accepted: "doit être accepté(e)"
+ empty: "doit être renseigné(e)"
+ blank: "doit être renseigné(e)"
+ too_long: "est trop long (pas plus de {{count}} caractères)"
+ too_short: "est trop court (au moins {{count}} caractères)"
+ wrong_length: "ne fait pas la bonne longueur (doit comporter {{count}} caractères)"
+ taken: "est déjà utilisé"
+ not_a_number: "n'est pas un nombre"
+ greater_than: "doit être supérieur à {{count}}"
+ greater_than_or_equal_to: "doit être supérieur ou égal à {{count}}"
+ equal_to: "doit être égal à {{count}}"
+ less_than: "doit être inférieur à {{count}}"
+ less_than_or_equal_to: "doit être inférieur ou égal à {{count}}"
+ odd: "doit être impair"
+ even: "doit être pair"
+ greater_than_start_date: "doit être postérieure à la date de début"
+ not_same_project: "n'appartient pas au même projet"
+ circular_dependency: "Cette relation créerait une dépendance circulaire"
+
+ actionview_instancetag_blank_option: Choisir
+
+ general_text_No: 'Non'
+ general_text_Yes: 'Oui'
+ general_text_no: 'non'
+ general_text_yes: 'oui'
+ general_lang_name: 'Français'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Le compte a été mis à jour avec succès.
+ notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
+ notice_account_password_updated: Mot de passe mis à jour avec succès.
+ notice_account_wrong_password: Mot de passe incorrect
+ notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé.
+ notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
+ notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
+ notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
+ notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
+ notice_successful_create: Création effectuée avec succès.
+ notice_successful_update: Mise à jour effectuée avec succès.
+ notice_successful_delete: Suppression effectuée avec succès.
+ notice_successful_connection: Connection réussie.
+ notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
+ notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
+ notice_not_authorized: "Vous n'êtes pas autorisés à accéder à cette page."
+ notice_email_sent: "Un email a été envoyé à {{value}}"
+ notice_email_error: "Erreur lors de l'envoi de l'email ({{value}})"
+ notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
+ notice_failed_to_save_issues: "{{count}} demande(s) sur les {{total}} sélectionnées n'ont pas pu être mise(s) à jour: {{ids}}."
+ notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
+ notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
+ notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
+ notice_unable_delete_version: Impossible de supprimer cette version.
+
+ error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage: {{value}}"
+ error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
+ error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt: {{value}}"
+ error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
+ error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
+ error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
+
+ warning_attachments_not_saved: "{{count}} fichier(s) n'ont pas pu être sauvegardés."
+
+ mail_subject_lost_password: "Votre mot de passe {{value}}"
+ mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:'
+ mail_subject_register: "Activation de votre compte {{value}}"
+ mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant:'
+ mail_body_account_information_external: "Vous pouvez utiliser votre compte {{value}} pour vous connecter."
+ mail_body_account_information: Paramètres de connexion de votre compte
+ mail_subject_account_activation_request: "Demande d'activation d'un compte {{value}}"
+ mail_body_account_activation_request: "Un nouvel utilisateur ({{value}}) s'est inscrit. Son compte nécessite votre approbation:"
+ mail_subject_reminder: "{{count}} demande(s) arrivent à échéance"
+ mail_body_reminder: "{{count}} demande(s) qui vous sont assignées arrivent à échéance dans les {{days}} prochains jours:"
+ mail_subject_wiki_content_added: "Page wiki '{{page}}' ajoutée"
+ mail_body_wiki_content_added: "La page wiki '{{page}}' a été ajoutée par {{author}}."
+ mail_subject_wiki_content_updated: "Page wiki '{{page}}' mise à jour"
+ mail_body_wiki_content_updated: "La page wiki '{{page}}' a été mise à jour par {{author}}."
+
+ gui_validation_error: 1 erreur
+ gui_validation_error_plural: "{{count}} erreurs"
+
+ field_name: Nom
+ field_description: Description
+ field_summary: Résumé
+ field_is_required: Obligatoire
+ field_firstname: Prénom
+ field_lastname: Nom
+ field_mail: Email
+ field_filename: Fichier
+ field_filesize: Taille
+ field_downloads: Téléchargements
+ field_author: Auteur
+ field_created_on: Créé
+ field_updated_on: Mis à jour
+ field_field_format: Format
+ field_is_for_all: Pour tous les projets
+ field_possible_values: Valeurs possibles
+ field_regexp: Expression régulière
+ field_min_length: Longueur minimum
+ field_max_length: Longueur maximum
+ field_value: Valeur
+ field_category: Catégorie
+ field_title: Titre
+ field_project: Projet
+ field_issue: Demande
+ field_status: Statut
+ field_notes: Notes
+ field_is_closed: Demande fermée
+ field_is_default: Valeur par défaut
+ field_tracker: Tracker
+ field_subject: Sujet
+ field_due_date: Echéance
+ field_assigned_to: Assigné à
+ field_priority: Priorité
+ field_fixed_version: Version cible
+ field_user: Utilisateur
+ field_role: Rôle
+ field_homepage: Site web
+ field_is_public: Public
+ field_parent: Sous-projet de
+ field_is_in_chlog: Demandes affichées dans l'historique
+ field_is_in_roadmap: Demandes affichées dans la roadmap
+ field_login: Identifiant
+ field_mail_notification: Notifications par mail
+ field_admin: Administrateur
+ field_last_login_on: Dernière connexion
+ field_language: Langue
+ field_effective_date: Date
+ field_password: Mot de passe
+ field_new_password: Nouveau mot de passe
+ field_password_confirmation: Confirmation
+ field_version: Version
+ field_type: Type
+ field_host: Hôte
+ field_port: Port
+ field_account: Compte
+ field_base_dn: Base DN
+ field_attr_login: Attribut Identifiant
+ field_attr_firstname: Attribut Prénom
+ field_attr_lastname: Attribut Nom
+ field_attr_mail: Attribut Email
+ field_onthefly: Création des utilisateurs à la volée
+ field_start_date: Début
+ field_done_ratio: % Réalisé
+ field_auth_source: Mode d'authentification
+ field_hide_mail: Cacher mon adresse mail
+ field_comments: Commentaire
+ field_url: URL
+ field_start_page: Page de démarrage
+ field_subproject: Sous-projet
+ field_hours: Heures
+ field_activity: Activité
+ field_spent_on: Date
+ field_identifier: Identifiant
+ field_is_filter: Utilisé comme filtre
+ field_issue_to: Demande liée
+ field_delay: Retard
+ field_assignable: Demandes assignables à ce rôle
+ field_redirect_existing_links: Rediriger les liens existants
+ field_estimated_hours: Temps estimé
+ field_column_names: Colonnes
+ field_time_zone: Fuseau horaire
+ field_searchable: Utilisé pour les recherches
+ field_default_value: Valeur par défaut
+ field_comments_sorting: Afficher les commentaires
+ field_parent_title: Page parent
+ field_editable: Modifiable
+ field_watcher: Observateur
+ field_identity_url: URL OpenID
+ field_content: Contenu
+ field_group_by: Grouper par
+
+ setting_app_title: Titre de l'application
+ setting_app_subtitle: Sous-titre de l'application
+ setting_welcome_text: Texte d'accueil
+ setting_default_language: Langue par défaut
+ setting_login_required: Authentification obligatoire
+ setting_self_registration: Inscription des nouveaux utilisateurs
+ setting_attachment_max_size: Taille max des fichiers
+ setting_issues_export_limit: Limite export demandes
+ setting_mail_from: Adresse d'émission
+ setting_bcc_recipients: Destinataires en copie cachée (cci)
+ setting_plain_text_mail: Mail texte brut (non HTML)
+ setting_host_name: Nom d'hôte et chemin
+ setting_text_formatting: Formatage du texte
+ setting_wiki_compression: Compression historique wiki
+ setting_feeds_limit: Limite du contenu des flux RSS
+ setting_default_projects_public: Définir les nouveaux projects comme publics par défaut
+ setting_autofetch_changesets: Récupération auto. des commits
+ setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
+ setting_commit_ref_keywords: Mot-clés de référencement
+ setting_commit_fix_keywords: Mot-clés de résolution
+ setting_autologin: Autologin
+ setting_date_format: Format de date
+ setting_time_format: Format d'heure
+ setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
+ setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
+ setting_repositories_encodings: Encodages des dépôts
+ setting_commit_logs_encoding: Encodage des messages de commit
+ setting_emails_footer: Pied-de-page des emails
+ setting_protocol: Protocole
+ setting_per_page_options: Options d'objets affichés par page
+ setting_user_format: Format d'affichage des utilisateurs
+ setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
+ setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
+ setting_enabled_scm: SCM activés
+ setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
+ setting_mail_handler_api_key: Clé de protection de l'API
+ setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
+ setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
+ setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
+ setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
+ setting_repository_log_display_limit: "Nombre maximum de revisions affichées sur l'historique d'un fichier"
+ setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
+ setting_password_min_length: Longueur minimum des mots de passe
+ setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
+ setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
+
+ permission_add_project: Créer un projet
+ permission_edit_project: Modifier le projet
+ permission_select_project_modules: Choisir les modules
+ permission_manage_members: Gérer les members
+ permission_manage_versions: Gérer les versions
+ permission_manage_categories: Gérer les catégories de demandes
+ permission_add_issues: Créer des demandes
+ permission_edit_issues: Modifier les demandes
+ permission_manage_issue_relations: Gérer les relations
+ permission_add_issue_notes: Ajouter des notes
+ permission_edit_issue_notes: Modifier les notes
+ permission_edit_own_issue_notes: Modifier ses propres notes
+ permission_move_issues: Déplacer les demandes
+ permission_delete_issues: Supprimer les demandes
+ permission_manage_public_queries: Gérer les requêtes publiques
+ permission_save_queries: Sauvegarder les requêtes
+ permission_view_gantt: Voir le gantt
+ permission_view_calendar: Voir le calendrier
+ permission_view_issue_watchers: Voir la liste des observateurs
+ permission_add_issue_watchers: Ajouter des observateurs
+ permission_delete_issue_watchers: Supprimer des observateurs
+ permission_log_time: Saisir le temps passé
+ permission_view_time_entries: Voir le temps passé
+ permission_edit_time_entries: Modifier les temps passés
+ permission_edit_own_time_entries: Modifier son propre temps passé
+ permission_manage_news: Gérer les annonces
+ permission_comment_news: Commenter les annonces
+ permission_manage_documents: Gérer les documents
+ permission_view_documents: Voir les documents
+ permission_manage_files: Gérer les fichiers
+ permission_view_files: Voir les fichiers
+ permission_manage_wiki: Gérer le wiki
+ permission_rename_wiki_pages: Renommer les pages
+ permission_delete_wiki_pages: Supprimer les pages
+ permission_view_wiki_pages: Voir le wiki
+ permission_view_wiki_edits: "Voir l'historique des modifications"
+ permission_edit_wiki_pages: Modifier les pages
+ permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
+ permission_protect_wiki_pages: Protéger les pages
+ permission_manage_repository: Gérer le dépôt de sources
+ permission_browse_repository: Parcourir les sources
+ permission_view_changesets: Voir les révisions
+ permission_commit_access: Droit de commit
+ permission_manage_boards: Gérer les forums
+ permission_view_messages: Voir les messages
+ permission_add_messages: Poster un message
+ permission_edit_messages: Modifier les messages
+ permission_edit_own_messages: Modifier ses propres messages
+ permission_delete_messages: Supprimer les messages
+ permission_delete_own_messages: Supprimer ses propres messages
+
+ project_module_issue_tracking: Suivi des demandes
+ project_module_time_tracking: Suivi du temps passé
+ project_module_news: Publication d'annonces
+ project_module_documents: Publication de documents
+ project_module_files: Publication de fichiers
+ project_module_wiki: Wiki
+ project_module_repository: Dépôt de sources
+ project_module_boards: Forums de discussion
+
+ label_user: Utilisateur
+ label_user_plural: Utilisateurs
+ label_user_new: Nouvel utilisateur
+ label_user_anonymous: Anonyme
+ label_project: Projet
+ label_project_new: Nouveau projet
+ label_project_plural: Projets
+ label_x_projects:
+ zero: aucun projet
+ one: 1 projet
+ other: "{{count}} projets"
+ label_project_all: Tous les projets
+ label_project_latest: Derniers projets
+ label_issue: Demande
+ label_issue_new: Nouvelle demande
+ label_issue_plural: Demandes
+ label_issue_view_all: Voir toutes les demandes
+ label_issue_added: Demande ajoutée
+ label_issue_updated: Demande mise à jour
+ label_issues_by: "Demandes par {{value}}"
+ label_document: Document
+ label_document_new: Nouveau document
+ label_document_plural: Documents
+ label_document_added: Document ajouté
+ label_role: Rôle
+ label_role_plural: Rôles
+ label_role_new: Nouveau rôle
+ label_role_and_permissions: Rôles et permissions
+ label_member: Membre
+ label_member_new: Nouveau membre
+ label_member_plural: Membres
+ label_tracker: Tracker
+ label_tracker_plural: Trackers
+ label_tracker_new: Nouveau tracker
+ label_workflow: Workflow
+ label_issue_status: Statut de demandes
+ label_issue_status_plural: Statuts de demandes
+ label_issue_status_new: Nouveau statut
+ label_issue_category: Catégorie de demandes
+ label_issue_category_plural: Catégories de demandes
+ label_issue_category_new: Nouvelle catégorie
+ label_custom_field: Champ personnalisé
+ label_custom_field_plural: Champs personnalisés
+ label_custom_field_new: Nouveau champ personnalisé
+ label_enumerations: Listes de valeurs
+ label_enumeration_new: Nouvelle valeur
+ label_information: Information
+ label_information_plural: Informations
+ label_please_login: Identification
+ label_register: S'enregistrer
+ label_login_with_open_id_option: S'authentifier avec OpenID
+ label_password_lost: Mot de passe perdu
+ label_home: Accueil
+ label_my_page: Ma page
+ label_my_account: Mon compte
+ label_my_projects: Mes projets
+ label_administration: Administration
+ label_login: Connexion
+ label_logout: Déconnexion
+ label_help: Aide
+ label_reported_issues: Demandes soumises
+ label_assigned_to_me_issues: Demandes qui me sont assignées
+ label_last_login: Dernière connexion
+ label_registered_on: Inscrit le
+ label_activity: Activité
+ label_overall_activity: Activité globale
+ label_user_activity: "Activité de {{value}}"
+ label_new: Nouveau
+ label_logged_as: Connecté en tant que
+ label_environment: Environnement
+ label_authentication: Authentification
+ label_auth_source: Mode d'authentification
+ label_auth_source_new: Nouveau mode d'authentification
+ label_auth_source_plural: Modes d'authentification
+ label_subproject_plural: Sous-projets
+ label_and_its_subprojects: "{{value}} et ses sous-projets"
+ label_min_max_length: Longueurs mini - maxi
+ label_list: Liste
+ label_date: Date
+ label_integer: Entier
+ label_float: Nombre décimal
+ label_boolean: Booléen
+ label_string: Texte
+ label_text: Texte long
+ label_attribute: Attribut
+ label_attribute_plural: Attributs
+ label_download: "{{count}} Téléchargement"
+ label_download_plural: "{{count}} Téléchargements"
+ label_no_data: Aucune donnée à afficher
+ label_change_status: Changer le statut
+ label_history: Historique
+ label_attachment: Fichier
+ label_attachment_new: Nouveau fichier
+ label_attachment_delete: Supprimer le fichier
+ label_attachment_plural: Fichiers
+ label_file_added: Fichier ajouté
+ label_report: Rapport
+ label_report_plural: Rapports
+ label_news: Annonce
+ label_news_new: Nouvelle annonce
+ label_news_plural: Annonces
+ label_news_latest: Dernières annonces
+ label_news_view_all: Voir toutes les annonces
+ label_news_added: Annonce ajoutée
+ label_change_log: Historique
+ label_settings: Configuration
+ label_overview: Aperçu
+ label_version: Version
+ label_version_new: Nouvelle version
+ label_version_plural: Versions
+ label_confirmation: Confirmation
+ label_export_to: 'Formats disponibles:'
+ label_read: Lire...
+ label_public_projects: Projets publics
+ label_open_issues: ouvert
+ label_open_issues_plural: ouverts
+ label_closed_issues: fermé
+ label_closed_issues_plural: fermés
+ label_x_open_issues_abbr_on_total:
+ zero: 0 ouvert sur {{total}}
+ one: 1 ouvert sur {{total}}
+ other: "{{count}} ouverts sur {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 ouvert
+ one: 1 ouvert
+ other: "{{count}} ouverts"
+ label_x_closed_issues_abbr:
+ zero: 0 fermé
+ one: 1 fermé
+ other: "{{count}} fermés"
+ label_total: Total
+ label_permissions: Permissions
+ label_current_status: Statut actuel
+ label_new_statuses_allowed: Nouveaux statuts autorisés
+ label_all: tous
+ label_none: aucun
+ label_nobody: personne
+ label_next: Suivant
+ label_previous: Précédent
+ label_used_by: Utilisé par
+ label_details: Détails
+ label_add_note: Ajouter une note
+ label_per_page: Par page
+ label_calendar: Calendrier
+ label_months_from: mois depuis
+ label_gantt: Gantt
+ label_internal: Interne
+ label_last_changes: "{{count}} derniers changements"
+ label_change_view_all: Voir tous les changements
+ label_personalize_page: Personnaliser cette page
+ label_comment: Commentaire
+ label_comment_plural: Commentaires
+ label_x_comments:
+ zero: aucun commentaire
+ one: 1 commentaire
+ other: "{{count}} commentaires"
+ label_comment_add: Ajouter un commentaire
+ label_comment_added: Commentaire ajouté
+ label_comment_delete: Supprimer les commentaires
+ label_query: Rapport personnalisé
+ label_query_plural: Rapports personnalisés
+ label_query_new: Nouveau rapport
+ label_filter_add: Ajouter le filtre
+ label_filter_plural: Filtres
+ label_equals: égal
+ label_not_equals: différent
+ label_in_less_than: dans moins de
+ label_in_more_than: dans plus de
+ label_in: dans
+ label_today: aujourd'hui
+ label_all_time: toute la période
+ label_yesterday: hier
+ label_this_week: cette semaine
+ label_last_week: la semaine dernière
+ label_last_n_days: "les {{count}} derniers jours"
+ label_this_month: ce mois-ci
+ label_last_month: le mois dernier
+ label_this_year: cette année
+ label_date_range: Période
+ label_less_than_ago: il y a moins de
+ label_more_than_ago: il y a plus de
+ label_ago: il y a
+ label_contains: contient
+ label_not_contains: ne contient pas
+ label_day_plural: jours
+ label_repository: Dépôt
+ label_repository_plural: Dépôts
+ label_browse: Parcourir
+ label_modification: "{{count}} modification"
+ label_modification_plural: "{{count}} modifications"
+ label_revision: Révision
+ label_revision_plural: Révisions
+ label_associated_revisions: Révisions associées
+ label_added: ajouté
+ label_modified: modifié
+ label_copied: copié
+ label_renamed: renommé
+ label_deleted: supprimé
+ label_latest_revision: Dernière révision
+ label_latest_revision_plural: Dernières révisions
+ label_view_revisions: Voir les révisions
+ label_max_size: Taille maximale
+ label_sort_highest: Remonter en premier
+ label_sort_higher: Remonter
+ label_sort_lower: Descendre
+ label_sort_lowest: Descendre en dernier
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Echéance dans {{value}}"
+ label_roadmap_overdue: "En retard de {{value}}"
+ label_roadmap_no_issues: Aucune demande pour cette version
+ label_search: Recherche
+ label_result_plural: Résultats
+ label_all_words: Tous les mots
+ label_wiki: Wiki
+ label_wiki_edit: Révision wiki
+ label_wiki_edit_plural: Révisions wiki
+ label_wiki_page: Page wiki
+ label_wiki_page_plural: Pages wiki
+ label_index_by_title: Index par titre
+ label_index_by_date: Index par date
+ label_current_version: Version actuelle
+ label_preview: Prévisualisation
+ label_feed_plural: Flux RSS
+ label_changes_details: Détails de tous les changements
+ label_issue_tracking: Suivi des demandes
+ label_spent_time: Temps passé
+ label_f_hour: "{{value}} heure"
+ label_f_hour_plural: "{{value}} heures"
+ label_time_tracking: Suivi du temps
+ label_change_plural: Changements
+ label_statistics: Statistiques
+ label_commits_per_month: Commits par mois
+ label_commits_per_author: Commits par auteur
+ label_view_diff: Voir les différences
+ label_diff_inline: en ligne
+ label_diff_side_by_side: côte à côte
+ label_options: Options
+ label_copy_workflow_from: Copier le workflow de
+ label_permissions_report: Synthèse des permissions
+ label_watched_issues: Demandes surveillées
+ label_related_issues: Demandes liées
+ label_applied_status: Statut appliqué
+ label_loading: Chargement...
+ label_relation_new: Nouvelle relation
+ label_relation_delete: Supprimer la relation
+ label_relates_to: lié à
+ label_duplicates: duplique
+ label_duplicated_by: dupliqué par
+ label_blocks: bloque
+ label_blocked_by: bloqué par
+ label_precedes: précède
+ label_follows: suit
+ label_end_to_start: fin à début
+ label_end_to_end: fin à fin
+ label_start_to_start: début à début
+ label_start_to_end: début à fin
+ label_stay_logged_in: Rester connecté
+ label_disabled: désactivé
+ label_show_completed_versions: Voir les versions passées
+ label_me: moi
+ label_board: Forum
+ label_board_new: Nouveau forum
+ label_board_plural: Forums
+ label_topic_plural: Discussions
+ label_message_plural: Messages
+ label_message_last: Dernier message
+ label_message_new: Nouveau message
+ label_message_posted: Message ajouté
+ label_reply_plural: Réponses
+ label_send_information: Envoyer les informations à l'utilisateur
+ label_year: Année
+ label_month: Mois
+ label_week: Semaine
+ label_date_from: Du
+ label_date_to: Au
+ label_language_based: Basé sur la langue de l'utilisateur
+ label_sort_by: "Trier par {{value}}"
+ label_send_test_email: Envoyer un email de test
+ label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a {{value}}"
+ label_module_plural: Modules
+ label_added_time_by: "Ajouté par {{author}} il y a {{age}}"
+ label_updated_time_by: "Mis à jour par {{author}} il y a {{age}}"
+ label_updated_time: "Mis à jour il y a {{value}}"
+ label_jump_to_a_project: Aller à un projet...
+ label_file_plural: Fichiers
+ label_changeset_plural: Révisions
+ label_default_columns: Colonnes par défaut
+ label_no_change_option: (Pas de changement)
+ label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
+ label_theme: Thème
+ label_default: Défaut
+ label_search_titles_only: Uniquement dans les titres
+ label_user_mail_option_all: "Pour tous les événements de tous mes projets"
+ label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
+ label_user_mail_option_none: "Seulement pour ce que je surveille ou à quoi je participe"
+ label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
+ label_registration_activation_by_email: activation du compte par email
+ label_registration_manual_activation: activation manuelle du compte
+ label_registration_automatic_activation: activation automatique du compte
+ label_display_per_page: "Par page: {{value}}"
+ label_age: Age
+ label_change_properties: Changer les propriétés
+ label_general: Général
+ label_more: Plus
+ label_scm: SCM
+ label_plugins: Plugins
+ label_ldap_authentication: Authentification LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Description facultative
+ label_add_another_file: Ajouter un autre fichier
+ label_preferences: Préférences
+ label_chronological_order: Dans l'ordre chronologique
+ label_reverse_chronological_order: Dans l'ordre chronologique inverse
+ label_planning: Planning
+ label_incoming_emails: Emails entrants
+ label_generate_key: Générer une clé
+ label_issue_watchers: Observateurs
+ label_example: Exemple
+ label_display: Affichage
+ label_sort: Tri
+ label_ascending: Croissant
+ label_descending: Décroissant
+ label_date_from_to: Du {{start}} au {{end}}
+ label_wiki_content_added: Page wiki ajoutée
+ label_wiki_content_updated: Page wiki mise à jour
+ label_group_plural: Groupes
+ label_group: Groupe
+ label_group_new: Nouveau groupe
+ label_time_entry_plural: Temps passé
+
+ button_login: Connexion
+ button_submit: Soumettre
+ button_save: Sauvegarder
+ button_check_all: Tout cocher
+ button_uncheck_all: Tout décocher
+ button_delete: Supprimer
+ button_create: Créer
+ button_create_and_continue: Créer et continuer
+ button_test: Tester
+ button_edit: Modifier
+ button_add: Ajouter
+ button_change: Changer
+ button_apply: Appliquer
+ button_clear: Effacer
+ button_lock: Verrouiller
+ button_unlock: Déverrouiller
+ button_download: Télécharger
+ button_list: Lister
+ button_view: Voir
+ button_move: Déplacer
+ button_move_and_follow: Déplacer et suivre
+ button_back: Retour
+ button_cancel: Annuler
+ button_activate: Activer
+ button_sort: Trier
+ button_log_time: Saisir temps
+ button_rollback: Revenir à cette version
+ button_watch: Surveiller
+ button_unwatch: Ne plus surveiller
+ button_reply: Répondre
+ button_archive: Archiver
+ button_unarchive: Désarchiver
+ button_reset: Réinitialiser
+ button_rename: Renommer
+ button_change_password: Changer de mot de passe
+ button_copy: Copier
+ button_annotate: Annoter
+ button_update: Mettre à jour
+ button_configure: Configurer
+ button_quote: Citer
+
+ status_active: actif
+ status_registered: enregistré
+ status_locked: vérouillé
+
+ version_status_open: ouvert
+ version_status_locked: vérouillé
+ version_status_closed: fermé
+
+ text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 pour aucune restriction
+ text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
+ text_subprojects_destroy_warning: "Ses sous-projets: {{value}} seront également supprimés."
+ text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
+ text_are_you_sure: Etes-vous sûr ?
+ text_tip_task_begin_day: tâche commençant ce jour
+ text_tip_task_end_day: tâche finissant ce jour
+ text_tip_task_begin_end_day: tâche commençant et finissant ce jour
+ text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
+ text_caracters_maximum: "{{count}} caractères maximum."
+ text_caracters_minimum: "{{count}} caractères minimum."
+ text_length_between: "Longueur comprise entre {{min}} et {{max}} caractères."
+ text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
+ text_unallowed_characters: Caractères non autorisés
+ text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
+ text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
+ text_issue_added: "La demande {{id}} a été soumise par {{author}}."
+ text_issue_updated: "La demande {{id}} a été mise à jour par {{author}}."
+ text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
+ text_issue_category_destroy_question: "{{count}} demandes sont affectées à cette catégories. Que voulez-vous faire ?"
+ text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
+ text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
+ text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)."
+ text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé."
+ text_load_default_configuration: Charger le paramétrage par défaut
+ text_status_changed_by_changeset: "Appliqué par commit {{value}}."
+ text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?'
+ text_select_project_modules: 'Selectionner les modules à activer pour ce project:'
+ text_default_administrator_account_changed: Compte administrateur par défaut changé
+ text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
+ text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
+ text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
+ text_destroy_time_entries_question: "{{hours}} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
+ text_destroy_time_entries: Supprimer les heures
+ text_assign_time_entries_to_project: Reporter les heures sur le projet
+ text_reassign_time_entries: 'Reporter les heures sur cette demande:'
+ text_user_wrote: "{{value}} a écrit:"
+ text_enumeration_destroy_question: "Cette valeur est affectée à {{count}} objets."
+ text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
+ text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/email.yml et redémarrez l'application pour les activer."
+ text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés."
+ text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
+ text_custom_field_possible_values_info: 'Une ligne par valeur'
+ text_wiki_page_destroy_question: "Cette page possède {{descendants}} sous-page(s) et descendante(s). Que voulez-vous faire ?"
+ text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
+ text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
+ text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
+
+ default_role_manager: Manager
+ default_role_developper: Développeur
+ default_role_reporter: Rapporteur
+ default_tracker_bug: Anomalie
+ default_tracker_feature: Evolution
+ default_tracker_support: Assistance
+ default_issue_status_new: Nouveau
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Résolu
+ default_issue_status_feedback: Commentaire
+ default_issue_status_closed: Fermé
+ default_issue_status_rejected: Rejeté
+ default_doc_category_user: Documentation utilisateur
+ default_doc_category_tech: Documentation technique
+ default_priority_low: Bas
+ default_priority_normal: Normal
+ default_priority_high: Haut
+ default_priority_urgent: Urgent
+ default_priority_immediate: Immédiat
+ default_activity_design: Conception
+ default_activity_development: Développement
+
+ enumeration_issue_priorities: Priorités des demandes
+ enumeration_doc_categories: Catégories des documents
+ enumeration_activities: Activités (suivi du temps)
+ label_greater_or_equal: ">="
+ label_less_or_equal: "<="
+ label_view_all_revisions: Voir toutes les révisions
+ label_tag: Tag
+ label_branch: Branche
+ error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
+ error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
+ text_journal_changed: "{{label}} changé de {{old}} à {{new}}"
+ text_journal_set_to: "{{label}} mis à {{value}}"
+ text_journal_deleted: "{{label}} {{old}} supprimé"
+ text_journal_added: "{{label}} {{value}} ajouté"
+ field_active: Actif
+ enumeration_system_activity: Activité système
+ setting_gravatar_default: Default Gravatar image
--- /dev/null
+# Galician (Spain) for Ruby on Rails
+# by Marcos Arias Pena (markus@agil-e.com)
+
+gl:
+ number:
+ format:
+ separator: ","
+ delimiter: "."
+ precision: 3
+
+ currency:
+ format:
+ format: "%n %u"
+ unit: "€"
+ separator: ","
+ delimiter: "."
+ precision: 2
+
+ percentage:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+
+ precision:
+ format:
+ # separator:
+ delimiter: ""
+ # precision:
+
+ human:
+ format:
+ # separator:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+
+ date:
+ formats:
+ default: "%e/%m/%Y"
+ short: "%e %b"
+ long: "%A %e de %B de %Y"
+ day_names: [Domingo, Luns, Martes, Mércores, Xoves, Venres, Sábado]
+ abbr_day_names: [Dom, Lun, Mar, Mer, Xov, Ven, Sab]
+ month_names: [~, Xaneiro, Febreiro, Marzo, Abril, Maio, Xunio, Xullo, Agosto, Setembro, Outubro, Novembro, Decembro]
+ abbr_month_names: [~, Xan, Feb, Maz, Abr, Mai, Xun, Xul, Ago, Set, Out, Nov, Dec]
+ order: [:day, :month, :year]
+
+ time:
+ formats:
+ default: "%A, %e de %B de %Y, %H:%M hs"
+ time: "%H:%M hs"
+ short: "%e/%m, %H:%M hs"
+ long: "%A %e de %B de %Y ás %H:%M horas"
+
+ am: ''
+ pm: ''
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'medio minuto'
+ less_than_x_seconds:
+ zero: 'menos dun segundo'
+ one: '1 segundo'
+ few: 'poucos segundos'
+ other: '{{count}} segundos'
+ x_seconds:
+ one: '1 segundo'
+ other: '{{count}} segundos'
+ less_than_x_minutes:
+ zero: 'menos dun minuto'
+ one: '1 minuto'
+ other: '{{count}} minutos'
+ x_minutes:
+ one: '1 minuto'
+ other: '{{count}} minuto'
+ about_x_hours:
+ one: 'aproximadamente unha hora'
+ other: '{{count}} horas'
+ x_days:
+ one: '1 día'
+ other: '{{count}} días'
+ x_weeks:
+ one: '1 semana'
+ other: '{{count}} semanas'
+ about_x_months:
+ one: 'aproximadamente 1 mes'
+ other: '{{count}} meses'
+ x_months:
+ one: '1 mes'
+ other: '{{count}} meses'
+ about_x_years:
+ one: 'aproximadamente 1 ano'
+ other: '{{count}} anos'
+ over_x_years:
+ one: 'máis dun ano'
+ other: '{{count}} anos'
+ now: 'agora'
+ today: 'hoxe'
+ tomorrow: 'mañá'
+ in: 'dentro de'
+
+ support:
+ array:
+ sentence_connector: e
+
+ activerecord:
+ models:
+ attributes:
+ errors:
+ template:
+ header:
+ one: "1 erro evitou que se poidese gardar o {{model}}"
+ other: "{{count}} erros evitaron que se poidese gardar o {{model}}"
+ body: "Atopáronse os seguintes problemas:"
+ messages:
+ inclusion: "non está incluido na lista"
+ exclusion: "xa existe"
+ invalid: "non é válido"
+ confirmation: "non coincide coa confirmación"
+ accepted: "debe ser aceptado"
+ empty: "non pode estar valeiro"
+ blank: "non pode estar en blanco"
+ too_long: "é demasiado longo (non máis de {{count}} carácteres)"
+ too_short: "é demasiado curto (non menos de {{count}} carácteres)"
+ wrong_length: "non ten a lonxitude correcta (debe ser de {{count}} carácteres)"
+ taken: "non está dispoñible"
+ not_a_number: "non é un número"
+ greater_than: "debe ser maior que {{count}}"
+ greater_than_or_equal_to: "debe ser maior ou igual que {{count}}"
+ equal_to: "debe ser igual a {{count}}"
+ less_than: "debe ser menor que {{count}}"
+ less_than_or_equal_to: "debe ser menor ou igual que {{count}}"
+ odd: "debe ser par"
+ even: "debe ser impar"
+ greater_than_start_date: "debe ser posterior á data de comezo"
+ not_same_project: "non pertence ao mesmo proxecto"
+ circular_dependency: "Esta relación podería crear unha dependencia circular"
+
+ actionview_instancetag_blank_option: Por favor seleccione
+
+ button_activate: Activar
+ button_add: Engadir
+ button_annotate: Anotar
+ button_apply: Aceptar
+ button_archive: Arquivar
+ button_back: Atrás
+ button_cancel: Cancelar
+ button_change: Cambiar
+ button_change_password: Cambiar contrasinal
+ button_check_all: Seleccionar todo
+ button_clear: Anular
+ button_configure: Configurar
+ button_copy: Copiar
+ button_create: Crear
+ button_delete: Borrar
+ button_download: Descargar
+ button_edit: Modificar
+ button_list: Listar
+ button_lock: Bloquear
+ button_log_time: Tempo dedicado
+ button_login: Conexión
+ button_move: Mover
+ button_quote: Citar
+ button_rename: Renomear
+ button_reply: Respostar
+ button_reset: Restablecer
+ button_rollback: Volver a esta versión
+ button_save: Gardar
+ button_sort: Ordenar
+ button_submit: Aceptar
+ button_test: Probar
+ button_unarchive: Desarquivar
+ button_uncheck_all: Non seleccionar nada
+ button_unlock: Desbloquear
+ button_unwatch: Non monitorizar
+ button_update: Actualizar
+ button_view: Ver
+ button_watch: Monitorizar
+ default_activity_design: Deseño
+ default_activity_development: Desenvolvemento
+ default_doc_category_tech: Documentación técnica
+ default_doc_category_user: Documentación de usuario
+ default_issue_status_in_progress: In Progress
+ default_issue_status_closed: Pechada
+ default_issue_status_feedback: Comentarios
+ default_issue_status_new: Nova
+ default_issue_status_rejected: Rexeitada
+ default_issue_status_resolved: Resolta
+ default_priority_high: Alta
+ default_priority_immediate: Inmediata
+ default_priority_low: Baixa
+ default_priority_normal: Normal
+ default_priority_urgent: Urxente
+ default_role_developper: Desenvolvedor
+ default_role_manager: Xefe de proxecto
+ default_role_reporter: Informador
+ default_tracker_bug: Erros
+ default_tracker_feature: Tarefas
+ default_tracker_support: Soporte
+ enumeration_activities: Actividades (tempo dedicado)
+ enumeration_doc_categories: Categorías do documento
+ enumeration_issue_priorities: Prioridade das peticións
+ error_can_t_load_default_data: "Non se puido cargar a configuración por defecto: {{value}}"
+ error_issue_not_found_in_project: 'A petición non se atopa ou non está asociada a este proxecto'
+ error_scm_annotate: "Non existe a entrada ou non se puido anotar"
+ error_scm_command_failed: "Aconteceu un erro ao acceder ó repositorio: {{value}}"
+ error_scm_not_found: "A entrada e/ou revisión non existe no repositorio."
+ field_account: Conta
+ field_activity: Actividade
+ field_admin: Administrador
+ field_assignable: Pódense asignar peticións a este perfil
+ field_assigned_to: Asignado a
+ field_attr_firstname: Atributo do nome
+ field_attr_lastname: Atributo do apelido
+ field_attr_login: Atributo do identificador
+ field_attr_mail: Atributo do Email
+ field_auth_source: Modo de identificación
+ field_author: Autor
+ field_base_dn: DN base
+ field_category: Categoría
+ field_column_names: Columnas
+ field_comments: Comentario
+ field_comments_sorting: Mostrar comentarios
+ field_created_on: Creado
+ field_default_value: Estado por defecto
+ field_delay: Retraso
+ field_description: Descrición
+ field_done_ratio: % Realizado
+ field_downloads: Descargas
+ field_due_date: Data fin
+ field_effective_date: Data
+ field_estimated_hours: Tempo estimado
+ field_field_format: Formato
+ field_filename: Arquivo
+ field_filesize: Tamaño
+ field_firstname: Nome
+ field_fixed_version: Versión prevista
+ field_hide_mail: Ocultar a miña dirección de correo
+ field_homepage: Sitio web
+ field_host: Anfitrión
+ field_hours: Horas
+ field_identifier: Identificador
+ field_is_closed: Petición resolta
+ field_is_default: Estado por defecto
+ field_is_filter: Usado como filtro
+ field_is_for_all: Para todos os proxectos
+ field_is_in_chlog: Consultar as peticións no histórico
+ field_is_in_roadmap: Consultar as peticións na planificación
+ field_is_public: Público
+ field_is_required: Obrigatorio
+ field_issue: Petición
+ field_issue_to: Petición relacionada
+ field_language: Idioma
+ field_last_login_on: Última conexión
+ field_lastname: Apelido
+ field_login: Identificador
+ field_mail: Correo electrónico
+ field_mail_notification: Notificacións por correo
+ field_max_length: Lonxitude máxima
+ field_min_length: Lonxitude mínima
+ field_name: Nome
+ field_new_password: Novo contrasinal
+ field_notes: Notas
+ field_onthefly: Creación do usuario "ao voo"
+ field_parent: Proxecto pai
+ field_parent_title: Páxina pai
+ field_password: Contrasinal
+ field_password_confirmation: Confirmación
+ field_port: Porto
+ field_possible_values: Valores posibles
+ field_priority: Prioridade
+ field_project: Proxecto
+ field_redirect_existing_links: Redireccionar enlaces existentes
+ field_regexp: Expresión regular
+ field_role: Perfil
+ field_searchable: Incluír nas búsquedas
+ field_spent_on: Data
+ field_start_date: Data de inicio
+ field_start_page: Páxina principal
+ field_status: Estado
+ field_subject: Tema
+ field_subproject: Proxecto secundario
+ field_summary: Resumo
+ field_time_zone: Zona horaria
+ field_title: Título
+ field_tracker: Tipo
+ field_type: Tipo
+ field_updated_on: Actualizado
+ field_url: URL
+ field_user: Usuario
+ field_value: Valor
+ field_version: Versión
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-15
+ general_csv_separator: ';'
+ general_first_day_of_week: '1'
+ general_lang_name: 'Galego'
+ general_pdf_encoding: ISO-8859-15
+ general_text_No: 'Non'
+ general_text_Yes: 'Si'
+ general_text_no: 'non'
+ general_text_yes: 'si'
+ gui_validation_error: 1 erro
+ gui_validation_error_plural: "{{count}} erros"
+ label_activity: Actividade
+ label_add_another_file: Engadir outro arquivo
+ label_add_note: Engadir unha nota
+ label_added: engadido
+ label_added_time_by: "Engadido por {{author}} fai {{age}}"
+ label_administration: Administración
+ label_age: Idade
+ label_ago: fai
+ label_all: todos
+ label_all_time: todo o tempo
+ label_all_words: Tódalas palabras
+ label_and_its_subprojects: "{{value}} e proxectos secundarios"
+ label_applied_status: Aplicar estado
+ label_assigned_to_me_issues: Peticións asignadas a min
+ label_associated_revisions: Revisións asociadas
+ label_attachment: Arquivo
+ label_attachment_delete: Borrar o arquivo
+ label_attachment_new: Novo arquivo
+ label_attachment_plural: Arquivos
+ label_attribute: Atributo
+ label_attribute_plural: Atributos
+ label_auth_source: Modo de autenticación
+ label_auth_source_new: Novo modo de autenticación
+ label_auth_source_plural: Modos de autenticación
+ label_authentication: Autenticación
+ label_blocked_by: bloqueado por
+ label_blocks: bloquea a
+ label_board: Foro
+ label_board_new: Novo foro
+ label_board_plural: Foros
+ label_boolean: Booleano
+ label_browse: Ollar
+ label_bulk_edit_selected_issues: Editar as peticións seleccionadas
+ label_calendar: Calendario
+ label_change_log: Cambios
+ label_change_plural: Cambios
+ label_change_properties: Cambiar propiedades
+ label_change_status: Cambiar o estado
+ label_change_view_all: Ver tódolos cambios
+ label_changes_details: Detalles de tódolos cambios
+ label_changeset_plural: Cambios
+ label_chronological_order: En orde cronolóxica
+ label_closed_issues: pechada
+ label_closed_issues_plural: pechadas
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_comment: Comentario
+ label_comment_add: Engadir un comentario
+ label_comment_added: Comentario engadido
+ label_comment_delete: Borrar comentarios
+ label_comment_plural: Comentarios
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_commits_per_author: Commits por autor
+ label_commits_per_month: Commits por mes
+ label_confirmation: Confirmación
+ label_contains: conten
+ label_copied: copiado
+ label_copy_workflow_from: Copiar fluxo de traballo dende
+ label_current_status: Estado actual
+ label_current_version: Versión actual
+ label_custom_field: Campo personalizado
+ label_custom_field_new: Novo campo personalizado
+ label_custom_field_plural: Campos personalizados
+ label_date: Data
+ label_date_from: Dende
+ label_date_range: Rango de datas
+ label_date_to: Ata
+ label_day_plural: días
+ label_default: Por defecto
+ label_default_columns: Columnas por defecto
+ label_deleted: suprimido
+ label_details: Detalles
+ label_diff_inline: en liña
+ label_diff_side_by_side: cara a cara
+ label_disabled: deshabilitado
+ label_display_per_page: "Por páxina: {{value}}"
+ label_document: Documento
+ label_document_added: Documento engadido
+ label_document_new: Novo documento
+ label_document_plural: Documentos
+ label_download: "{{count}} Descarga"
+ label_download_plural: "{{count}} Descargas"
+ label_downloads_abbr: D/L
+ label_duplicated_by: duplicada por
+ label_duplicates: duplicada de
+ label_end_to_end: fin a fin
+ label_end_to_start: fin a principio
+ label_enumeration_new: Novo valor
+ label_enumerations: Listas de valores
+ label_environment: Entorno
+ label_equals: igual
+ label_example: Exemplo
+ label_export_to: 'Exportar a:'
+ label_f_hour: "{{value}} hora"
+ label_f_hour_plural: "{{value}} horas"
+ label_feed_plural: Feeds
+ label_feeds_access_key_created_on: "Clave de acceso por RSS creada fai {{value}}"
+ label_file_added: Arquivo engadido
+ label_file_plural: Arquivos
+ label_filter_add: Engadir o filtro
+ label_filter_plural: Filtros
+ label_float: Flotante
+ label_follows: posterior a
+ label_gantt: Gantt
+ label_general: Xeral
+ label_generate_key: Xerar clave
+ label_help: Axuda
+ label_history: Histórico
+ label_home: Inicio
+ label_in: en
+ label_in_less_than: en menos que
+ label_in_more_than: en mais que
+ label_incoming_emails: Correos entrantes
+ label_index_by_date: Índice por data
+ label_index_by_title: Índice por título
+ label_information: Información
+ label_information_plural: Información
+ label_integer: Número
+ label_internal: Interno
+ label_issue: Petición
+ label_issue_added: Petición engadida
+ label_issue_category: Categoría das peticións
+ label_issue_category_new: Nova categoría
+ label_issue_category_plural: Categorías das peticións
+ label_issue_new: Nova petición
+ label_issue_plural: Peticións
+ label_issue_status: Estado da petición
+ label_issue_status_new: Novo estado
+ label_issue_status_plural: Estados das peticións
+ label_issue_tracking: Peticións
+ label_issue_updated: Petición actualizada
+ label_issue_view_all: Ver tódalas peticións
+ label_issue_watchers: Seguidores
+ label_issues_by: "Peticións por {{value}}"
+ label_jump_to_a_project: Ir ao proxecto...
+ label_language_based: Baseado no idioma
+ label_last_changes: "últimos {{count}} cambios"
+ label_last_login: Última conexión
+ label_last_month: último mes
+ label_last_n_days: "últimos {{count}} días"
+ label_last_week: última semana
+ label_latest_revision: Última revisión
+ label_latest_revision_plural: Últimas revisións
+ label_ldap_authentication: Autenticación LDAP
+ label_less_than_ago: fai menos de
+ label_list: Lista
+ label_loading: Cargando...
+ label_logged_as: Conectado como
+ label_login: Conexión
+ label_logout: Desconexión
+ label_max_size: Tamaño máximo
+ label_me: eu mesmo
+ label_member: Membro
+ label_member_new: Novo membro
+ label_member_plural: Membros
+ label_message_last: Última mensaxe
+ label_message_new: Nova mensaxe
+ label_message_plural: Mensaxes
+ label_message_posted: Mensaxe engadida
+ label_min_max_length: Lonxitude mín - máx
+ label_modification: "{{count}} modificación"
+ label_modification_plural: "{{count}} modificacións"
+ label_modified: modificado
+ label_module_plural: Módulos
+ label_month: Mes
+ label_months_from: meses de
+ label_more: Mais
+ label_more_than_ago: fai mais de
+ label_my_account: A miña conta
+ label_my_page: A miña páxina
+ label_my_projects: Os meus proxectos
+ label_new: Novo
+ label_new_statuses_allowed: Novos estados autorizados
+ label_news: Noticia
+ label_news_added: Noticia engadida
+ label_news_latest: Últimas noticias
+ label_news_new: Nova noticia
+ label_news_plural: Noticias
+ label_news_view_all: Ver tódalas noticias
+ label_next: Seguinte
+ label_no_change_option: (Sen cambios)
+ label_no_data: Ningún dato a mostrar
+ label_nobody: ninguén
+ label_none: ningún
+ label_not_contains: non conten
+ label_not_equals: non igual
+ label_open_issues: aberta
+ label_open_issues_plural: abertas
+ label_optional_description: Descrición opcional
+ label_options: Opcións
+ label_overall_activity: Actividade global
+ label_overview: Vistazo
+ label_password_lost: ¿Esqueciches o contrasinal?
+ label_per_page: Por páxina
+ label_permissions: Permisos
+ label_permissions_report: Informe de permisos
+ label_personalize_page: Personalizar esta páxina
+ label_planning: Planificación
+ label_please_login: Conexión
+ label_plugins: Extensións
+ label_precedes: anterior a
+ label_preferences: Preferencias
+ label_preview: Previsualizar
+ label_previous: Anterior
+ label_project: Proxecto
+ label_project_all: Tódolos proxectos
+ label_project_latest: Últimos proxectos
+ label_project_new: Novo proxecto
+ label_project_plural: Proxectos
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_public_projects: Proxectos públicos
+ label_query: Consulta personalizada
+ label_query_new: Nova consulta
+ label_query_plural: Consultas personalizadas
+ label_read: Ler...
+ label_register: Rexistrar
+ label_registered_on: Inscrito o
+ label_registration_activation_by_email: activación de conta por correo
+ label_registration_automatic_activation: activación automática de conta
+ label_registration_manual_activation: activación manual de conta
+ label_related_issues: Peticións relacionadas
+ label_relates_to: relacionada con
+ label_relation_delete: Eliminar relación
+ label_relation_new: Nova relación
+ label_renamed: renomeado
+ label_reply_plural: Respostas
+ label_report: Informe
+ label_report_plural: Informes
+ label_reported_issues: Peticións rexistradas por min
+ label_repository: Repositorio
+ label_repository_plural: Repositorios
+ label_result_plural: Resultados
+ label_reverse_chronological_order: En orde cronolóxica inversa
+ label_revision: Revisión
+ label_revision_plural: Revisións
+ label_roadmap: Planificación
+ label_roadmap_due_in: "Remata en {{value}}"
+ label_roadmap_no_issues: Non hai peticións para esta versión
+ label_roadmap_overdue: "{{value}} tarde"
+ label_role: Perfil
+ label_role_and_permissions: Perfiles e permisos
+ label_role_new: Novo perfil
+ label_role_plural: Perfiles
+ label_scm: SCM
+ label_search: Búsqueda
+ label_search_titles_only: Buscar só en títulos
+ label_send_information: Enviar información da conta ó usuario
+ label_send_test_email: Enviar un correo de proba
+ label_settings: Configuración
+ label_show_completed_versions: Mostra as versións rematadas
+ label_sort_by: "Ordenar por {{value}}"
+ label_sort_higher: Subir
+ label_sort_highest: Primeiro
+ label_sort_lower: Baixar
+ label_sort_lowest: Último
+ label_spent_time: Tempo dedicado
+ label_start_to_end: comezo a fin
+ label_start_to_start: comezo a comezo
+ label_statistics: Estatísticas
+ label_stay_logged_in: Lembrar contrasinal
+ label_string: Texto
+ label_subproject_plural: Proxectos secundarios
+ label_text: Texto largo
+ label_theme: Tema
+ label_this_month: este mes
+ label_this_week: esta semana
+ label_this_year: este ano
+ label_time_tracking: Control de tempo
+ label_today: hoxe
+ label_topic_plural: Temas
+ label_total: Total
+ label_tracker: Tipo
+ label_tracker_new: Novo tipo
+ label_tracker_plural: Tipos de peticións
+ label_updated_time: "Actualizado fai {{value}}"
+ label_updated_time_by: "Actualizado por {{author}} fai {{age}}"
+ label_used_by: Utilizado por
+ label_user: Usuario
+ label_user_activity: "Actividade de {{value}}"
+ label_user_mail_no_self_notified: "Non quero ser avisado de cambios feitos por min"
+ label_user_mail_option_all: "Para calquera evento en tódolos proxectos"
+ label_user_mail_option_none: "Só para elementos monitorizados ou relacionados comigo"
+ label_user_mail_option_selected: "Para calquera evento dos proxectos seleccionados..."
+ label_user_new: Novo usuario
+ label_user_plural: Usuarios
+ label_version: Versión
+ label_version_new: Nova versión
+ label_version_plural: Versións
+ label_view_diff: Ver diferencias
+ label_view_revisions: Ver as revisións
+ label_watched_issues: Peticións monitorizadas
+ label_week: Semana
+ label_wiki: Wiki
+ label_wiki_edit: Wiki edición
+ label_wiki_edit_plural: Wiki edicións
+ label_wiki_page: Wiki páxina
+ label_wiki_page_plural: Wiki páxinas
+ label_workflow: Fluxo de traballo
+ label_year: Ano
+ label_yesterday: onte
+ mail_body_account_activation_request: "Inscribiuse un novo usuario ({{value}}). A conta está pendente de aprobación:"
+ mail_body_account_information: Información sobre a súa conta
+ mail_body_account_information_external: "Pode usar a súa conta {{value}} para conectarse."
+ mail_body_lost_password: 'Para cambiar o seu contrasinal, faga clic no seguinte enlace:'
+ mail_body_register: 'Para activar a súa conta, faga clic no seguinte enlace:'
+ mail_body_reminder: "{{count}} petición(s) asignadas a ti rematan nos próximos {{days}} días:"
+ mail_subject_account_activation_request: "Petición de activación de conta {{value}}"
+ mail_subject_lost_password: "O teu contrasinal de {{value}}"
+ mail_subject_register: "Activación da conta de {{value}}"
+ mail_subject_reminder: "{{count}} petición(s) rematarán nos próximos días"
+ notice_account_activated: A súa conta foi activada. Xa pode conectarse.
+ notice_account_invalid_creditentials: Usuario ou contrasinal inválido.
+ notice_account_lost_email_sent: Enviouse un correo con instrucións para elixir un novo contrasinal.
+ notice_account_password_updated: Contrasinal modificado correctamente.
+ notice_account_pending: "A súa conta creouse e está pendente da aprobación por parte do administrador."
+ notice_account_register_done: Conta creada correctamente. Para activala, faga clic sobre o enlace que se lle enviou por correo.
+ notice_account_unknown_email: Usuario descoñecido.
+ notice_account_updated: Conta actualizada correctamente.
+ notice_account_wrong_password: Contrasinal incorrecto.
+ notice_can_t_change_password: Esta conta utiliza unha fonte de autenticación externa. Non é posible cambiar o contrasinal.
+ notice_default_data_loaded: Configuración por defecto cargada correctamente.
+ notice_email_error: "Ocorreu un error enviando o correo ({{value}})"
+ notice_email_sent: "Enviouse un correo a {{value}}"
+ notice_failed_to_save_issues: "Imposible gravar %s petición(s) en {{count}} seleccionado: {{ids}}."
+ notice_feeds_access_key_reseted: A súa clave de acceso para RSS reiniciouse.
+ notice_file_not_found: A páxina á que tenta acceder non existe.
+ notice_locking_conflict: Os datos modificáronse por outro usuario.
+ notice_no_issue_selected: "Ningunha petición seleccionada. Por favor, comprobe a petición que quere modificar"
+ notice_not_authorized: Non ten autorización para acceder a esta páxina.
+ notice_successful_connection: Conexión correcta.
+ notice_successful_create: Creación correcta.
+ notice_successful_delete: Borrado correcto.
+ notice_successful_update: Modificación correcta.
+ notice_unable_delete_version: Non se pode borrar a versión
+ permission_add_issue_notes: Engadir notas
+ permission_add_issue_watchers: Engadir seguidores
+ permission_add_issues: Engadir peticións
+ permission_add_messages: Enviar mensaxes
+ permission_browse_repository: Ollar repositorio
+ permission_comment_news: Comentar noticias
+ permission_commit_access: Acceso de escritura
+ permission_delete_issues: Borrar peticións
+ permission_delete_messages: Borrar mensaxes
+ permission_delete_own_messages: Borrar mensaxes propios
+ permission_delete_wiki_pages: Borrar páxinas wiki
+ permission_delete_wiki_pages_attachments: Borrar arquivos
+ permission_edit_issue_notes: Modificar notas
+ permission_edit_issues: Modificar peticións
+ permission_edit_messages: Modificar mensaxes
+ permission_edit_own_issue_notes: Modificar notas propias
+ permission_edit_own_messages: Editar mensaxes propios
+ permission_edit_own_time_entries: Modificar tempos dedicados propios
+ permission_edit_project: Modificar proxecto
+ permission_edit_time_entries: Modificar tempos dedicados
+ permission_edit_wiki_pages: Modificar páxinas wiki
+ permission_log_time: Anotar tempo dedicado
+ permission_manage_boards: Administrar foros
+ permission_manage_categories: Administrar categorías de peticións
+ permission_manage_documents: Administrar documentos
+ permission_manage_files: Administrar arquivos
+ permission_manage_issue_relations: Administrar relación con outras peticións
+ permission_manage_members: Administrar membros
+ permission_manage_news: Administrar noticias
+ permission_manage_public_queries: Administrar consultas públicas
+ permission_manage_repository: Administrar repositorio
+ permission_manage_versions: Administrar versións
+ permission_manage_wiki: Administrar wiki
+ permission_move_issues: Mover peticións
+ permission_protect_wiki_pages: Protexer páxinas wiki
+ permission_rename_wiki_pages: Renomear páxinas wiki
+ permission_save_queries: Gravar consultas
+ permission_select_project_modules: Seleccionar módulos do proxecto
+ permission_view_calendar: Ver calendario
+ permission_view_changesets: Ver cambios
+ permission_view_documents: Ver documentos
+ permission_view_files: Ver arquivos
+ permission_view_gantt: Ver diagrama de Gantt
+ permission_view_issue_watchers: Ver lista de seguidores
+ permission_view_messages: Ver mensaxes
+ permission_view_time_entries: Ver tempo dedicado
+ permission_view_wiki_edits: Ver histórico do wiki
+ permission_view_wiki_pages: Ver wiki
+ project_module_boards: Foros
+ project_module_documents: Documentos
+ project_module_files: Arquivos
+ project_module_issue_tracking: Peticións
+ project_module_news: Noticias
+ project_module_repository: Repositorio
+ project_module_time_tracking: Control de tempo
+ project_module_wiki: Wiki
+ setting_activity_days_default: Días a mostrar na actividade do proxecto
+ setting_app_subtitle: Subtítulo da aplicación
+ setting_app_title: Título da aplicación
+ setting_attachment_max_size: Tamaño máximo do arquivo
+ setting_autofetch_changesets: Autorechear os commits do repositorio
+ setting_autologin: Conexión automática
+ setting_bcc_recipients: Ocultar as copias de carbón (bcc)
+ setting_commit_fix_keywords: Palabras clave para a corrección
+ setting_commit_logs_encoding: Codificación das mensaxes de commit
+ setting_commit_ref_keywords: Palabras clave para a referencia
+ setting_cross_project_issue_relations: Permitir relacionar peticións de distintos proxectos
+ setting_date_format: Formato da data
+ setting_default_language: Idioma por defecto
+ setting_default_projects_public: Os proxectos novos son públicos por defecto
+ setting_diff_max_lines_displayed: Número máximo de diferencias mostradas
+ setting_display_subprojects_issues: Mostrar por defecto peticións de prox. secundarios no principal
+ setting_emails_footer: Pe de mensaxes
+ setting_enabled_scm: Activar SCM
+ setting_feeds_limit: Límite de contido para sindicación
+ setting_gravatar_enabled: Usar iconas de usuario (Gravatar)
+ setting_host_name: Nome e ruta do servidor
+ setting_issue_list_default_columns: Columnas por defecto para a lista de peticións
+ setting_issues_export_limit: Límite de exportación de peticións
+ setting_login_required: Requírese identificación
+ setting_mail_from: Correo dende o que enviar mensaxes
+ setting_mail_handler_api_enabled: Activar SW para mensaxes entrantes
+ setting_mail_handler_api_key: Clave da API
+ setting_per_page_options: Obxectos por páxina
+ setting_plain_text_mail: só texto plano (non HTML)
+ setting_protocol: Protocolo
+ setting_repositories_encodings: Codificacións do repositorio
+ setting_self_registration: Rexistro permitido
+ setting_sequential_project_identifiers: Xerar identificadores de proxecto
+ setting_sys_api_enabled: Habilitar SW para a xestión do repositorio
+ setting_text_formatting: Formato de texto
+ setting_time_format: Formato de hora
+ setting_user_format: Formato de nome de usuario
+ setting_welcome_text: Texto de benvida
+ setting_wiki_compression: Compresión do historial do Wiki
+ status_active: activo
+ status_locked: bloqueado
+ status_registered: rexistrado
+ text_are_you_sure: ¿Está seguro?
+ text_assign_time_entries_to_project: Asignar as horas ó proxecto
+ text_caracters_maximum: "{{count}} caracteres como máximo."
+ text_caracters_minimum: "{{count}} caracteres como mínimo"
+ text_comma_separated: Múltiples valores permitidos (separados por coma).
+ text_default_administrator_account_changed: Conta de administrador por defecto modificada
+ text_destroy_time_entries: Borrar as horas
+ text_destroy_time_entries_question: Existen {{hours}} horas asignadas á petición que quere borrar. ¿Que quere facer ?
+ text_diff_truncated: '... Diferencia truncada por exceder o máximo tamaño visualizable.'
+ text_email_delivery_not_configured: "O envío de correos non está configurado, e as notificacións desactiváronse. \n Configure o servidor de SMTP en config/email.yml e reinicie a aplicación para activar os cambios."
+ text_enumeration_category_reassign_to: 'Reasignar ó seguinte valor:'
+ text_enumeration_destroy_question: "{{count}} obxectos con este valor asignado."
+ text_file_repository_writable: Pódese escribir no repositorio
+ text_issue_added: "Petición {{id}} engadida por {{author}}."
+ text_issue_category_destroy_assignments: Deixar as peticións sen categoría
+ text_issue_category_destroy_question: "Algunhas peticións ({{count}}) están asignadas a esta categoría. ¿Que desexa facer?"
+ text_issue_category_reassign_to: Reasignar as peticións á categoría
+ text_issue_updated: "A petición {{id}} actualizouse por {{author}}."
+ text_issues_destroy_confirmation: '¿Seguro que quere borrar as peticións seleccionadas?'
+ text_issues_ref_in_commit_messages: Referencia e petición de corrección nas mensaxes
+ text_length_between: "Lonxitude entre {{min}} e {{max}} caracteres."
+ text_load_default_configuration: Cargar a configuración por defecto
+ text_min_max_length_info: 0 para ningunha restrición
+ text_no_configuration_data: "Inda non se configuraron perfiles, nin tipos, estados e fluxo de traballo asociado a peticións. Recoméndase encarecidamente cargar a configuración por defecto. Unha vez cargada, poderá modificala."
+ text_project_destroy_confirmation: ¿Estás seguro de querer eliminar o proxecto?
+ text_project_identifier_info: 'Letras minúsculas (a-z), números e signos de puntuación permitidos.<br />Unha vez gardado, o identificador non pode modificarse.'
+ text_reassign_time_entries: 'Reasignar as horas a esta petición:'
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_repository_usernames_mapping: "Estableza a correspondencia entre os usuarios de Redmine e os presentes no log do repositorio.\nOs usuarios co mesmo nome ou correo en Redmine e no repositorio serán asociados automaticamente."
+ text_rmagick_available: RMagick dispoñible (opcional)
+ text_select_mail_notifications: Seleccionar os eventos a notificar
+ text_select_project_modules: 'Seleccione os módulos a activar para este proxecto:'
+ text_status_changed_by_changeset: "Aplicado nos cambios {{value}}"
+ text_subprojects_destroy_warning: "Os proxectos secundarios: {{value}} tamén se eliminarán"
+ text_tip_task_begin_day: tarefa que comeza este día
+ text_tip_task_begin_end_day: tarefa que comeza e remata este día
+ text_tip_task_end_day: tarefa que remata este día
+ text_tracker_no_workflow: Non hai ningún fluxo de traballo definido para este tipo de petición
+ text_unallowed_characters: Caracteres non permitidos
+ text_user_mail_option: "Dos proxectos non seleccionados, só recibirá notificacións sobre elementos monitorizados ou elementos nos que estea involucrado (por exemplo, peticións das que vostede sexa autor ou asignadas a vostede)."
+ text_user_wrote: "{{value}} escribiu:"
+ text_wiki_destroy_confirmation: ¿Seguro que quere borrar o wiki e todo o seu contido?
+ text_workflow_edit: Seleccionar un fluxo de traballo para actualizar
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ field_editable: Editable
+ text_plugin_assets_writable: Plugin assets directory writable
+ label_display: Display
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Hebrew translations for Ruby on Rails
+# by Dotan Nahum (dipidi@gmail.com)
+
+he:
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%e %b"
+ long: "%B %e, %Y"
+ only_day: "%e"
+
+ day_names: [ראשון, שני, שלישי, רביעי, חמישי, שישי, שבת]
+ abbr_day_names: [רא, שנ, של, רב, חמ, שי, שב]
+ month_names: [~, ינואר, פברואר, מרץ, אפריל, מאי, יוני, יולי, אוגוסט, ספטמבר, אוקטובר, נובמבר, דצמבר]
+ abbr_month_names: [~, יאנ, פב, מרץ, אפר, מאי, יונ, יול, אוג, ספט, אוק, נוב, דצ]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%a %b %d %H:%M:%S %Z %Y"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ only_second: "%S"
+
+ datetime:
+ formats:
+ default: "%d-%m-%YT%H:%M:%S%Z"
+
+ am: 'am'
+ pm: 'pm'
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'חצי דקה'
+ less_than_x_seconds:
+ zero: 'פחות משניה אחת'
+ one: 'פחות משניה אחת'
+ other: 'פחות מ- {{count}} שניות'
+ x_seconds:
+ one: 'שניה אחת'
+ other: '{{count}} שניות'
+ less_than_x_minutes:
+ zero: 'פחות מדקה אחת'
+ one: 'פחות מדקה אחת'
+ other: 'פחות מ- {{count}} דקות'
+ x_minutes:
+ one: 'דקה אחת'
+ other: '{{count}} דקות'
+ about_x_hours:
+ one: 'בערך שעה אחת'
+ other: 'בערך {{count}} שעות'
+ x_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}} שנים'
+
+ number:
+ format:
+ precision: 3
+ separator: '.'
+ delimiter: ','
+ currency:
+ format:
+ unit: 'שח'
+ precision: 2
+ format: '%u %n'
+ human:
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "לא נכלל ברשימה"
+ exclusion: "לא זמין"
+ invalid: "לא ולידי"
+ confirmation: "לא תואם לאישורו"
+ accepted: "חייב באישור"
+ empty: "חייב להכלל"
+ blank: "חייב להכלל"
+ too_long: "יותר מדי ארוך (לא יותר מ- {{count}} תוים)"
+ too_short: "יותר מדי קצר (לא יותר מ- {{count}} תוים)"
+ wrong_length: "לא באורך הנכון (חייב להיות {{count}} תוים)"
+ taken: "לא זמין"
+ not_a_number: "הוא לא מספר"
+ greater_than: "חייב להיות גדול מ- {{count}}"
+ greater_than_or_equal_to: "חייב להיות גדול או שווה ל- {{count}}"
+ equal_to: "חייב להיות שווה ל- {{count}}"
+ less_than: "חייב להיות קטן מ- {{count}}"
+ less_than_or_equal_to: "חייב להיות קטן או שווה ל- {{count}}"
+ odd: "חייב להיות אי זוגי"
+ even: "חייב להיות זוגי"
+ greater_than_start_date: "חייב להיות מאוחר יותר מתאריך ההתחלה"
+ not_same_project: "לא שייך לאותו הפרויקט"
+ circular_dependency: "הקשר הזה יצור תלות מעגלית"
+
+ actionview_instancetag_blank_option: בחר בבקשה
+
+ general_text_No: 'לא'
+ general_text_Yes: 'כן'
+ general_text_no: 'לא'
+ general_text_yes: 'כן'
+ general_lang_name: 'Hebrew (עברית)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-8-I
+ general_pdf_encoding: ISO-8859-8-I
+ general_first_day_of_week: '7'
+
+ notice_account_updated: החשבון עודכן בהצלחה!
+ notice_account_invalid_creditentials: שם משתמש או סיסמה שגויים
+ notice_account_password_updated: הסיסמה עודכנה בהצלחה!
+ notice_account_wrong_password: סיסמה שגויה
+ notice_account_register_done: החשבון נוצר בהצלחה. להפעלת החשבון לחץ על הקישור שנשלח לדוא"ל שלך.
+ notice_account_unknown_email: משתמש לא מוכר.
+ notice_can_t_change_password: החשבון הזה משתמש במקור אימות חיצוני. שינוי סיסמה הינו בילתי אפשר
+ notice_account_lost_email_sent: דוא"ל עם הוראות לבחירת סיסמה חדשה נשלח אליך.
+ notice_account_activated: חשבונך הופעל. אתה יכול להתחבר כעת.
+ notice_successful_create: יצירה מוצלחת.
+ notice_successful_update: עידכון מוצלח.
+ notice_successful_delete: מחיקה מוצלחת.
+ notice_successful_connection: חיבור מוצלח.
+ notice_file_not_found: הדף שאת\ה מנסה לגשת אליו אינו קיים או שהוסר.
+ notice_locking_conflict: המידע עודכן על ידי משתמש אחר.
+ notice_not_authorized: אינך מורשה לראות דף זה.
+ notice_email_sent: "דואל נשלח לכתובת {{value}}"
+ notice_email_error: "ארעה שגיאה בעט שליחת הדואל ({{value}})"
+ notice_feeds_access_key_reseted: מפתח ה-RSS שלך אופס.
+ notice_failed_to_save_issues: "נכשרת בשמירת {{count}} נושא\ים ב {{total}} נבחרו: {{ids}}."
+ notice_no_issue_selected: "לא נבחר אף נושא! בחר בבקשה את הנושאים שברצונך לערוך."
+
+ error_scm_not_found: כניסה ו\או גירסא אינם קיימים במאגר.
+ error_scm_command_failed: "ארעה שגיאה בעת ניסון גישה למאגר: {{value}}"
+
+ mail_subject_lost_password: "סיסמת ה-{{value}} שלך"
+ mail_body_lost_password: 'לשינו סיסמת ה-Redmine שלך,לחץ על הקישור הבא:'
+ mail_subject_register: "הפעלת חשבון {{value}}"
+ mail_body_register: 'להפעלת חשבון ה-Redmine שלך, לחץ על הקישור הבא:'
+
+ gui_validation_error: שגיאה 1
+ gui_validation_error_plural: "{{count}} שגיאות"
+
+ field_name: שם
+ field_description: תיאור
+ field_summary: תקציר
+ field_is_required: נדרש
+ field_firstname: שם פרטי
+ field_lastname: שם משפחה
+ field_mail: דוא"ל
+ field_filename: קובץ
+ field_filesize: גודל
+ field_downloads: הורדות
+ field_author: כותב
+ field_created_on: נוצר
+ field_updated_on: עודכן
+ field_field_format: פורמט
+ field_is_for_all: לכל הפרויקטים
+ field_possible_values: ערכים אפשריים
+ field_regexp: ביטוי רגיל
+ field_min_length: אורך מינימאלי
+ field_max_length: אורך מקסימאלי
+ field_value: ערך
+ field_category: קטגוריה
+ field_title: כותרת
+ field_project: פרויקט
+ field_issue: נושא
+ field_status: מצב
+ field_notes: הערות
+ field_is_closed: נושא סגור
+ field_is_default: ערך ברירת מחדל
+ field_tracker: עוקב
+ field_subject: שם נושא
+ field_due_date: תאריך סיום
+ field_assigned_to: מוצב ל
+ field_priority: עדיפות
+ field_fixed_version: גירסאת יעד
+ field_user: מתשמש
+ field_role: תפקיד
+ field_homepage: דף הבית
+ field_is_public: פומבי
+ field_parent: תת פרויקט של
+ field_is_in_chlog: נושאים המוצגים בדו"ח השינויים
+ 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_default_value: ערך ברירת מחדל
+
+ 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_host_name: שם שרת
+ setting_text_formatting: עיצוב טקסט
+ setting_wiki_compression: כיווץ היסטורית WIKI
+ setting_feeds_limit: גבול תוכן הזנות
+ setting_autofetch_changesets: משיכה אוטומתי של עידכונים
+ setting_sys_api_enabled: אפשר WS לניהול המאגר
+ setting_commit_ref_keywords: מילות מפתח מקשרות
+ setting_commit_fix_keywords: מילות מפתח מתקנות
+ setting_autologin: חיבור אוטומטי
+ setting_date_format: פורמט תאריך
+ setting_cross_project_issue_relations: הרשה קישור נושאים בין פרויקטים
+ setting_issue_list_default_columns: עמודות ברירת מחדל המוצגות ברשימת הנושאים
+ setting_repositories_encodings: קידוד המאגרים
+
+ label_user: משתמש
+ label_user_plural: משתמשים
+ label_user_new: משתמש חדש
+ label_project: פרויקט
+ label_project_new: פרויקט חדש
+ label_project_plural: פרויקטים
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: כל הפרויקטים
+ label_project_latest: הפרויקטים החדשים ביותר
+ label_issue: נושא
+ label_issue_new: נושא חדש
+ label_issue_plural: נושאים
+ label_issue_view_all: צפה בכל הנושאים
+ label_document: מסמך
+ label_document_new: מסמך חדש
+ label_document_plural: מסמכים
+ label_role: תפקיד
+ label_role_plural: תפקידים
+ label_role_new: תפקיד חדש
+ label_role_and_permissions: תפקידים והרשאות
+ label_member: חבר
+ label_member_new: חבר חדש
+ label_member_plural: חברים
+ label_tracker: עוקב
+ label_tracker_plural: עוקבים
+ label_tracker_new: עוקב חדש
+ label_workflow: זרימת עבודה
+ label_issue_status: מצב נושא
+ label_issue_status_plural: מצבי נושא
+ label_issue_status_new: מצב חדש
+ label_issue_category: קטגורית נושא
+ label_issue_category_plural: קטגוריות נושא
+ label_issue_category_new: קטגוריה חדשה
+ label_custom_field: שדה אישי
+ label_custom_field_plural: שדות אישיים
+ label_custom_field_new: שדה אישי חדש
+ label_enumerations: אינומרציות
+ label_enumeration_new: ערך חדש
+ label_information: מידע
+ label_information_plural: מידע
+ label_please_login: התחבר בבקשה
+ label_register: הרשמה
+ label_password_lost: אבדה הסיסמה?
+ label_home: דף הבית
+ label_my_page: הדף שלי
+ label_my_account: החשבון שלי
+ label_my_projects: הפרויקטים שלי
+ label_administration: אדמיניסטרציה
+ label_login: התחבר
+ label_logout: התנתק
+ label_help: עזרה
+ label_reported_issues: נושאים שדווחו
+ label_assigned_to_me_issues: נושאים שהוצבו לי
+ label_last_login: חיבור אחרון
+ label_registered_on: נרשם בתאריך
+ label_activity: פעילות
+ label_new: חדש
+ label_logged_as: מחובר כ
+ label_environment: סביבה
+ label_authentication: אישור
+ label_auth_source: מצב אישור
+ label_auth_source_new: מצב אישור חדש
+ label_auth_source_plural: מצבי אישור
+ label_subproject_plural: תת-פרויקטים
+ label_min_max_length: אורך מינימאלי - מקסימאלי
+ label_list: רשימה
+ label_date: תאריך
+ label_integer: מספר שלם
+ label_boolean: ערך בוליאני
+ label_string: טקסט
+ label_text: טקסט ארוך
+ label_attribute: תכונה
+ label_attribute_plural: תכונות
+ label_download: "הורדה {{count}}"
+ label_download_plural: "{{count}} הורדות"
+ label_no_data: אין מידע להציג
+ label_change_status: שנה מצב
+ label_history: היסטוריה
+ label_attachment: קובץ
+ label_attachment_new: קובץ חדש
+ label_attachment_delete: מחק קובץ
+ label_attachment_plural: קבצים
+ label_report: דו"ח
+ label_report_plural: דו"חות
+ label_news: חדשות
+ label_news_new: הוסף חדשות
+ label_news_plural: חדשות
+ label_news_latest: חדשות אחרונות
+ label_news_view_all: צפה בכל החדשות
+ label_change_log: דו"ח שינויים
+ label_settings: הגדרות
+ label_overview: מבט רחב
+ label_version: גירסא
+ label_version_new: גירסא חדשה
+ label_version_plural: גירסאות
+ label_confirmation: אישור
+ label_export_to: יצא ל
+ label_read: קרא...
+ label_public_projects: פרויקטים פומביים
+ label_open_issues: פתוח
+ label_open_issues_plural: פתוחים
+ label_closed_issues: סגור
+ label_closed_issues_plural: סגורים
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: סה"כ
+ label_permissions: הרשאות
+ label_current_status: מצב נוכחי
+ label_new_statuses_allowed: מצבים חדשים אפשריים
+ label_all: הכל
+ label_none: כלום
+ label_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: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: הוסף תגובה
+ label_comment_added: תגובה הוספה
+ label_comment_delete: מחק תגובות
+ label_query: שאילתה אישית
+ label_query_plural: שאילתות אישיות
+ label_query_new: שאילתה חדשה
+ label_filter_add: הוסף מסנן
+ label_filter_plural: מסננים
+ label_equals: הוא
+ label_not_equals: הוא לא
+ label_in_less_than: בפחות מ
+ label_in_more_than: ביותר מ
+ label_in: ב
+ label_today: היום
+ label_this_week: השבוע
+ label_less_than_ago: פחות ממספר ימים
+ label_more_than_ago: יותר ממספר ימים
+ label_ago: מספר ימים
+ label_contains: מכיל
+ label_not_contains: לא מכיל
+ label_day_plural: ימים
+ label_repository: מאגר
+ label_browse: סייר
+ label_modification: "שינוי {{count}}"
+ label_modification_plural: "{{count}} שינויים"
+ label_revision: גירסא
+ label_revision_plural: גירסאות
+ label_added: הוסף
+ label_modified: שונה
+ label_deleted: נמחק
+ label_latest_revision: גירסא אחרונה
+ label_latest_revision_plural: גירסאות אחרונות
+ label_view_revisions: צפה בגירסאות
+ label_max_size: גודל מקסימאלי
+ label_sort_highest: הזז לראשית
+ label_sort_higher: הזז למעלה
+ label_sort_lower: הזז למטה
+ label_sort_lowest: הזז לתחתית
+ label_roadmap: מפת הדרכים
+ label_roadmap_due_in: "נגמר בעוד {{value}}"
+ label_roadmap_overdue: "{{value}} מאחר"
+ label_roadmap_no_issues: אין נושאים לגירסא זו
+ label_search: חפש
+ label_result_plural: תוצאות
+ label_all_words: כל המילים
+ label_wiki: Wiki
+ label_wiki_edit: ערוך Wiki
+ label_wiki_edit_plural: עריכות Wiki
+ label_wiki_page: דף Wiki
+ label_wiki_page_plural: דפי Wiki
+ label_index_by_title: סדר על פי כותרת
+ label_index_by_date: סדר על פי תאריך
+ label_current_version: גירסא נוכאית
+ label_preview: תצוגה מקדימה
+ label_feed_plural: הזנות
+ label_changes_details: פירוט כל השינויים
+ label_issue_tracking: מעקב אחר נושאים
+ label_spent_time: זמן שבוזבז
+ label_f_hour: "{{value}} שעה"
+ label_f_hour_plural: "{{value}} שעות"
+ label_time_tracking: מעקב זמנים
+ label_change_plural: שינויים
+ label_statistics: סטטיסטיקות
+ label_commits_per_month: הפקדות לפי חודש
+ label_commits_per_author: הפקדות לפי כותב
+ label_view_diff: צפה בהבדלים
+ label_diff_inline: בתוך השורה
+ label_diff_side_by_side: צד לצד
+ label_options: אפשרויות
+ label_copy_workflow_from: העתק זירמת עבודה מ
+ label_permissions_report: דו"ח הרשאות
+ label_watched_issues: נושאים שנצפו
+ label_related_issues: נושאים קשורים
+ label_applied_status: מוצב מוחל
+ label_loading: טוען...
+ label_relation_new: קשר חדש
+ label_relation_delete: מחק קשר
+ label_relates_to: קשור ל
+ label_duplicates: מכפיל את
+ label_blocks: חוסם את
+ label_blocked_by: חסום ע"י
+ label_precedes: מקדים את
+ label_follows: עוקב אחרי
+ label_end_to_start: מהתחלה לסוף
+ label_end_to_end: מהסוף לסוף
+ label_start_to_start: מהתחלה להתחלה
+ label_start_to_end: מהתחלה לסוף
+ label_stay_logged_in: השאר מחובר
+ label_disabled: מבוטל
+ label_show_completed_versions: הצג גירזאות גמורות
+ label_me: אני
+ label_board: פורום
+ label_board_new: פורום חדש
+ label_board_plural: פורומים
+ label_topic_plural: נושאים
+ label_message_plural: הודעות
+ label_message_last: הודעה אחרונה
+ label_message_new: הודעה חדשה
+ label_reply_plural: השבות
+ label_send_information: שלח מידע על חשבון למשתמש
+ label_year: שנה
+ label_month: חודש
+ label_week: שבוע
+ label_date_from: מתאריך
+ label_date_to: עד
+ label_language_based: מבוסס שפה
+ label_sort_by: "מין לפי {{value}}"
+ label_send_test_email: שלח דו"ל בדיקה
+ label_feeds_access_key_created_on: "מפתח הזנת RSS נוצר לפני{{value}}"
+ label_module_plural: מודולים
+ label_added_time_by: "הוסף על ידי {{author}} לפני {{age}} "
+ label_updated_time: "עודכן לפני {{value}} "
+ label_jump_to_a_project: קפוץ לפרויקט...
+ label_file_plural: קבצים
+ label_changeset_plural: אוסף שינוים
+ label_default_columns: עמודת ברירת מחדל
+ label_no_change_option: (אין שינוים)
+ label_bulk_edit_selected_issues: ערוך את הנושאים המסומנים
+ label_theme: ערכת נושא
+ label_default: ברירת מחדש
+
+ button_login: התחבר
+ button_submit: הגש
+ button_save: שמור
+ button_check_all: בחר הכל
+ button_uncheck_all: בחר כלום
+ button_delete: מחק
+ button_create: צור
+ button_test: בדוק
+ button_edit: ערוך
+ button_add: הוסף
+ button_change: שנה
+ button_apply: הוצא לפועל
+ button_clear: נקה
+ button_lock: נעל
+ button_unlock: בטל נעילה
+ button_download: הורד
+ button_list: רשימה
+ button_view: צפה
+ button_move: הזז
+ button_back: הקודם
+ button_cancel: בטח
+ button_activate: הפעל
+ button_sort: מיין
+ button_log_time: זמן לוג
+ button_rollback: חזור לגירסא זו
+ button_watch: צפה
+ button_unwatch: בטל צפיה
+ button_reply: השב
+ button_archive: ארכיון
+ button_unarchive: הוצא מהארכיון
+ button_reset: אפס
+ button_rename: שנה שם
+
+ status_active: פעיל
+ status_registered: רשום
+ status_locked: נעול
+
+ text_select_mail_notifications: בחר פעולת שבגללן ישלח דוא"ל.
+ text_regexp_info: כגון. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 משמעו ללא הגבלות
+ text_project_destroy_confirmation: האם אתה בטוח שברצונך למחוק את הפרויקט ואת כל המידע הקשור אליו ?
+ text_workflow_edit: בחר תפקיד ועוקב כדי לערות את זרימת העבודה
+ text_are_you_sure: האם אתה בטוח ?
+ text_tip_task_begin_day: מטלה המתחילה היום
+ text_tip_task_end_day: מטלה המסתיימת היום
+ text_tip_task_begin_end_day: מטלה המתחילה ומסתיימת היום
+ text_project_identifier_info: 'אותיות לטיניות (a-z), מספרים ומקפים.<br />ברגע שנשמר, לא ניתן לשנות את המזהה.'
+ text_caracters_maximum: "מקסימום {{count}} תווים."
+ text_length_between: "אורך בין {{min}} ל {{max}} תווים."
+ text_tracker_no_workflow: זרימת עבודה לא הוגדרה עבור עוקב זה
+ text_unallowed_characters: תווים לא מורשים
+ text_comma_separated: הכנסת ערכים מרובים מותרת (מופרדים בפסיקים).
+ text_issues_ref_in_commit_messages: קישור ותיקום נושאים בהודעות הפקדות
+ text_issue_added: "הנושא {{id}} דווח (by {{author}})."
+ text_issue_updated: "הנושא {{id}} עודכן (by {{author}})."
+ text_wiki_destroy_confirmation: האם אתה בטוח שברצונך למחוק את הWIKI הזה ואת כל תוכנו?
+ text_issue_category_destroy_question: "כמה נושאים ({{count}}) מוצבים לקטגוריה הזו. מה ברצונך לעשות?"
+ text_issue_category_destroy_assignments: הסר הצבת קטגוריה
+ text_issue_category_reassign_to: הצב מחדש את הקטגוריה לנושאים
+
+ default_role_manager: מנהל
+ default_role_developper: מפתח
+ default_role_reporter: מדווח
+ default_tracker_bug: באג
+ default_tracker_feature: פיצ'ר
+ default_tracker_support: תמיכה
+ default_issue_status_new: חדש
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: פתור
+ default_issue_status_feedback: משוב
+ default_issue_status_closed: סגור
+ default_issue_status_rejected: דחוי
+ default_doc_category_user: תיעוד משתמש
+ default_doc_category_tech: תיעוד טכני
+ default_priority_low: נמוכה
+ default_priority_normal: רגילה
+ default_priority_high: גהבוה
+ default_priority_urgent: דחופה
+ default_priority_immediate: מידית
+ default_activity_design: עיצוב
+ default_activity_development: פיתוח
+
+ enumeration_issue_priorities: עדיפות נושאים
+ enumeration_doc_categories: קטגוריות מסמכים
+ enumeration_activities: פעילויות (מעקב אחר זמנים)
+ label_search_titles_only: חפש בכותרות בלבד
+ label_nobody: אף אחד
+ button_change_password: שנה סיסמא
+ text_user_mail_option: "בפרויקטים שלא בחרת, אתה רק תקבל התרעות על שאתה צופה או קשור אליהם (לדוגמא:נושאים שאתה היוצר שלהם או מוצבים אליך)."
+ label_user_mail_option_selected: "לכל אירוע בפרויקטים שבחרתי בלבד..."
+ label_user_mail_option_all: "לכל אירוע בכל הפרויקטים שלי"
+ label_user_mail_option_none: "רק לנושאים שאני צופה או קשור אליהם"
+ setting_emails_footer: תחתית דוא"ל
+ label_float: צף
+ button_copy: העתק
+ mail_body_account_information_external: "אתה יכול להשתמש בחשבון {{value}} כדי להתחבר"
+ mail_body_account_information: פרטי החשבון שלך
+ setting_protocol: פרוטוקול
+ label_user_mail_no_self_notified: "אני לא רוצה שיודיעו לי על שינויים שאני מבצע"
+ setting_time_format: פורמט זמן
+ label_registration_activation_by_email: הפעל חשבון באמצעות דוא"ל
+ mail_subject_account_activation_request: "בקשת הפעלה לחשבון {{value}}"
+ mail_body_account_activation_request: "משתמש חדש ({{value}}) נרשם. החשבון שלו מחכה לאישור שלך:"
+ label_registration_automatic_activation: הפעלת חשבון אוטומטית
+ label_registration_manual_activation: הפעלת חשבון ידנית
+ notice_account_pending: "החשבון שלך נוצר ועתה מחכה לאישור מנהל המערכת."
+ field_time_zone: איזור זמן
+ text_caracters_minimum: "חייב להיות לפחות באורך של {{count}} תווים."
+ setting_bcc_recipients: מוסתר (bcc)
+ button_annotate: הוסף תיאור מסגרת
+ label_issues_by: "נושאים של {{value}}"
+ field_searchable: ניתן לחיפוש
+ label_display_per_page: "לכל דף: {{value}}"
+ setting_per_page_options: אפשרויות אוביקטים לפי דף
+ label_age: גיל
+ notice_default_data_loaded: אפשרויות ברירת מחדל מופעלות.
+ text_load_default_configuration: טען את אפשרויות ברירת המחדל
+ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. יהיה באפשרותך לשנותו לאחר שיטען."
+ error_can_t_load_default_data: "אפשרויות ברירת המחדל לא הצליחו להיטען: {{value}}"
+ button_update: עדכן
+ label_change_properties: שנה מאפיינים
+ label_general: כללי
+ label_repository_plural: מאגרים
+ label_associated_revisions: שינויים קשורים
+ setting_user_format: פורמט הצגת משתמשים
+ text_status_changed_by_changeset: "הוחל בסדרת השינויים {{value}}."
+ label_more: עוד
+ text_issues_destroy_confirmation: 'האם את\ה בטוח שברצונך למחוק את הנושא\ים ?'
+ label_scm: SCM
+ text_select_project_modules: 'בחר מודולים להחיל על פקרויקט זה:'
+ label_issue_added: נושא הוסף
+ label_issue_updated: נושא עודכן
+ label_document_added: מוסמך הוסף
+ label_message_posted: הודעה הוספה
+ label_file_added: קובץ הוסף
+ label_news_added: חדשות הוספו
+ project_module_boards: לוחות
+ project_module_issue_tracking: מעקב נושאים
+ project_module_wiki: Wiki
+ project_module_files: קבצים
+ project_module_documents: מסמכים
+ project_module_repository: מאגר
+ project_module_news: חדשות
+ project_module_time_tracking: מעקב אחר זמנים
+ text_file_repository_writable: מאגר הקבצים ניתן לכתיבה
+ text_default_administrator_account_changed: מנהל המערכת ברירת המחדל שונה
+ text_rmagick_available: RMagick available (optional)
+ button_configure: אפשרויות
+ label_plugins: פלאגינים
+ label_ldap_authentication: אימות LDAP
+ label_downloads_abbr: D/L
+ label_this_month: החודש
+ label_last_n_days: "ב-{{count}} ימים אחרונים"
+ label_all_time: תמיד
+ label_this_year: השנה
+ label_date_range: טווח תאריכים
+ label_last_week: שבוע שעבר
+ label_yesterday: אתמול
+ label_last_month: חודש שעבר
+ label_add_another_file: הוסף עוד קובץ
+ label_optional_description: תיאור רשות
+ text_destroy_time_entries_question: "{{hours}} שעות דווחו על הנושים שאת\ה עומד\ת למחוק. מה ברצונך לעשות ?"
+ error_issue_not_found_in_project: 'הנושאים לא נמצאו או אינם שיכים לפרויקט'
+ text_assign_time_entries_to_project: הצב שעות שדווחו לפרויקט הזה
+ text_destroy_time_entries: מחק שעות שדווחו
+ text_reassign_time_entries: 'הצב מחדש שעות שדווחו לפרויקט הזה:'
+ setting_activity_days_default: ימים המוצגים על פעילות הפרויקט
+ label_chronological_order: בסדר כרונולוגי
+ field_comments_sorting: הצג הערות
+ label_reverse_chronological_order: בסדר כרונולוגי הפוך
+ label_preferences: העדפות
+ setting_display_subprojects_issues: הצג נושאים של תת פרויקטים כברירת מחדל
+ label_overall_activity: פעילות כוללת
+ setting_default_projects_public: פרויקטים חדשים הינם פומביים כברירת מחדל
+ error_scm_annotate: "הכניסה לא קיימת או שלא ניתן לתאר אותה."
+ label_planning: תכנון
+ text_subprojects_destroy_warning: "תת הפרויקט\ים: {{value}} ימחקו גם כן."
+ label_and_its_subprojects: "{{value}} וכל תת הפרויקטים שלו"
+ mail_body_reminder: "{{count}} נושאים שמיועדים אליך מיועדים להגשה בתוך {{days}} ימים:"
+ mail_subject_reminder: "{{count}} נושאים מיעדים להגשה בימים הקרובים"
+ text_user_wrote: "{{value}} כתב:"
+ label_duplicated_by: שוכפל ע"י
+ setting_enabled_scm: אפשר SCM
+ text_enumeration_category_reassign_to: 'הצב מחדש לערך הזה:'
+ text_enumeration_destroy_question: "{{count}} אוביקטים מוצבים לערך זה."
+ label_incoming_emails: דוא"ל נכנס
+ label_generate_key: יצר מפתח
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: מפתח API
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: דף אב
+ label_issue_watchers: צופים
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: צטט
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: לא ניתן למחוק גירסא
+ label_renamed: השם שונה
+ label_copied: הועתק
+ setting_plain_text_mail: טקסט פשוט בלבד (ללא HTML)
+ permission_view_files: צפה בקבצים
+ permission_edit_issues: ערוך נושאים
+ permission_edit_own_time_entries: ערוך את לוג הזמן של עצמך
+ permission_manage_public_queries: נהל שאילתות פומביות
+ permission_add_issues: הוסף נושא
+ permission_log_time: תעד זמן שבוזבז
+ permission_view_changesets: צפה בקבוצות שינויים
+ permission_view_time_entries: צפה בזמן שבוזבז
+ permission_manage_versions: נהל גירסאות
+ permission_manage_wiki: נהל wiki
+ permission_manage_categories: נהל קטגוריות נושאים
+ permission_protect_wiki_pages: הגן כל דפי wiki
+ permission_comment_news: הגב על החדשות
+ permission_delete_messages: מחק הודעות
+ permission_select_project_modules: בחר מודולי פרויקט
+ permission_manage_documents: נהל מסמכים
+ permission_edit_wiki_pages: ערוך דפי wiki
+ permission_add_issue_watchers: הוסף צופים
+ permission_view_gantt: צפה בגאנט
+ permission_move_issues: הזז נושאים
+ permission_manage_issue_relations: נהל יחס בין נושאים
+ permission_delete_wiki_pages: מחק דפי wiki
+ permission_manage_boards: נהל לוחות
+ permission_delete_wiki_pages_attachments: מחק דבוקות
+ permission_view_wiki_edits: צפה בהיסטורית wiki
+ permission_add_messages: הצב הודעות
+ permission_view_messages: צפה בהודעות
+ permission_manage_files: נהל קבצים
+ permission_edit_issue_notes: ערוך רשימות
+ permission_manage_news: נהל חדשות
+ permission_view_calendar: צפה בלוח השנה
+ permission_manage_members: נהל חברים
+ permission_edit_messages: ערוך הודעות
+ permission_delete_issues: מחק נושאים
+ permission_view_issue_watchers: צפה ברשימה צופים
+ permission_manage_repository: נהל מאגר
+ permission_commit_access: Commit access
+ permission_browse_repository: סייר במאגר
+ permission_view_documents: צפה במסמכים
+ permission_edit_project: ערוך פרויקט
+ permission_add_issue_notes: Add notes
+ permission_save_queries: שמור שאילתות
+ permission_view_wiki_pages: צפה ב-wiki
+ permission_rename_wiki_pages: שנה שם של דפי wiki
+ permission_edit_time_entries: ערוך רישום זמנים
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: דוגמא
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: ערוך הודעות של עצמך
+ permission_delete_own_messages: מחק הודעות של עצמך
+ label_user_activity: "הפעילות של {{value}}"
+ label_updated_time_by: "עודכן ע'י {{author}} לפני {{age}}"
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Hungarian translations for Ruby on Rails
+# by Richard Abonyi (richard.abonyi@gmail.com)
+# thanks to KKata, replaced and #hup.hu
+# Cleaned up by László Bácsi (http://lackac.hu)
+# updated by kfl62 kfl62g@gmail.com
+
+"hu":
+ date:
+ formats:
+ default: "%Y.%m.%d."
+ short: "%b %e."
+ long: "%Y. %B %e."
+ day_names: [vasárnap, hétfő, kedd, szerda, csütörtök, péntek, szombat]
+ abbr_day_names: [v., h., k., sze., cs., p., szo.]
+ month_names: [~, január, február, március, április, május, június, július, augusztus, szeptember, október, november, december]
+ abbr_month_names: [~, jan., febr., márc., ápr., máj., jún., júl., aug., szept., okt., nov., dec.]
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%Y. %b %e., %H:%M"
+ time: "%H:%M"
+ short: "%b %e., %H:%M"
+ long: "%Y. %B %e., %A, %H:%M"
+ am: "de."
+ pm: "du."
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'fél perc'
+ less_than_x_seconds:
+# zero: 'kevesebb, mint 1 másodperc'
+ one: 'kevesebb, mint 1 másodperc'
+ other: 'kevesebb, mint {{count}} másodperc'
+ x_seconds:
+ one: '1 másodperc'
+ other: '{{count}} másodperc'
+ less_than_x_minutes:
+# zero: 'kevesebb, mint 1 perc'
+ one: 'kevesebb, mint 1 perc'
+ other: 'kevesebb, mint {{count}} perc'
+ x_minutes:
+ one: '1 perc'
+ other: '{{count}} perc'
+ about_x_hours:
+ one: 'majdnem 1 óra'
+ other: 'majdnem {{count}} óra'
+ x_days:
+ one: '1 nap'
+ other: '{{count}} nap'
+ about_x_months:
+ one: 'majdnem 1 hónap'
+ other: 'majdnem {{count}} hónap'
+ x_months:
+ one: '1 hónap'
+ other: '{{count}} hónap'
+ about_x_years:
+ one: 'majdnem 1 év'
+ other: 'majdnem {{count}} év'
+ over_x_years:
+ one: 'több, mint 1 év'
+ other: 'több, mint {{count}} év'
+ prompts:
+ year: "Év"
+ month: "Hónap"
+ day: "Nap"
+ hour: "Óra"
+ minute: "Perc"
+ second: "Másodperc"
+
+ number:
+ format:
+ precision: 2
+ separator: ','
+ delimiter: ' '
+ currency:
+ format:
+ unit: 'Ft'
+ precision: 0
+ format: '%n %u'
+ separator: ""
+ delimiter: ""
+ percentage:
+ format:
+ delimiter: ""
+ precision:
+ format:
+ delimiter: ""
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "bájt"
+ other: "bájt"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+# sentence_connector: "és"
+# skip_last_comma: true
+ words_connector: ", "
+ two_words_connector: " és "
+ last_word_connector: " és "
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "1 hiba miatt nem menthető a következő: {{model}}"
+ other: "{{count}} hiba miatt nem menthető a következő: {{model}}"
+ body: "Problémás mezők:"
+ messages:
+ inclusion: "nincs a listában"
+ exclusion: "nem elérhető"
+ invalid: "nem megfelelő"
+ confirmation: "nem egyezik"
+ accepted: "nincs elfogadva"
+ empty: "nincs megadva"
+ blank: "nincs megadva"
+ too_long: "túl hosszú (nem lehet több {{count}} karakternél)"
+ too_short: "túl rövid (legalább {{count}} karakter kell legyen)"
+ wrong_length: "nem megfelelő hosszúságú ({{count}} karakter szükséges)"
+ taken: "már foglalt"
+ not_a_number: "nem szám"
+ greater_than: "nagyobb kell legyen, mint {{count}}"
+ greater_than_or_equal_to: "legalább {{count}} kell legyen"
+ equal_to: "pontosan {{count}} kell legyen"
+ less_than: "kevesebb, mint {{count}} kell legyen"
+ less_than_or_equal_to: "legfeljebb {{count}} lehet"
+ odd: "páratlan kell legyen"
+ even: "páros kell legyen"
+ greater_than_start_date: "nagyobbnak kell lennie, mint az indítás dátuma"
+ not_same_project: "nem azonos projekthez tartozik"
+ circular_dependency: "Ez a kapcsolat egy körkörös függőséget eredményez"
+
+ actionview_instancetag_blank_option: Kérem válasszon
+
+ general_text_No: 'Nem'
+ general_text_Yes: 'Igen'
+ general_text_no: 'nem'
+ general_text_yes: 'igen'
+ general_lang_name: 'Magyar'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-2
+ general_pdf_encoding: ISO-8859-2
+ general_first_day_of_week: '1'
+
+ notice_account_updated: A fiók adatai sikeresen frissítve.
+ notice_account_invalid_creditentials: Hibás felhasználói név, vagy jelszó
+ notice_account_password_updated: A jelszó módosítása megtörtént.
+ notice_account_wrong_password: Hibás jelszó
+ notice_account_register_done: A fiók sikeresen létrehozva. Aktiválásához kattints az e-mailben kapott linkre
+ notice_account_unknown_email: Ismeretlen felhasználó.
+ notice_can_t_change_password: A fiók külső azonosítási forrást használ. A jelszó megváltoztatása nem lehetséges.
+ notice_account_lost_email_sent: Egy e-mail üzenetben postáztunk Önnek egy leírást az új jelszó beállításáról.
+ notice_account_activated: Fiókját aktiváltuk. Most már be tud jelentkezni a rendszerbe.
+ notice_successful_create: Sikeres létrehozás.
+ notice_successful_update: Sikeres módosítás.
+ notice_successful_delete: Sikeres törlés.
+ notice_successful_connection: Sikeres bejelentkezés.
+ notice_file_not_found: Az oldal, amit meg szeretne nézni nem található, vagy átkerült egy másik helyre.
+ notice_locking_conflict: Az adatot egy másik felhasználó idő közben módosította.
+ notice_not_authorized: Nincs hozzáférési engedélye ehhez az oldalhoz.
+ notice_email_sent: "Egy e-mail üzenetet küldtünk a következő címre {{value}}"
+ notice_email_error: "Hiba történt a levél küldése közben ({{value}})"
+ notice_feeds_access_key_reseted: Az RSS hozzáférési kulcsát újra generáltuk.
+ notice_failed_to_save_issues: "Nem sikerült a {{count}} feladat(ok) mentése a {{total}} -ban kiválasztva: {{ids}}."
+ notice_no_issue_selected: "Nincs feladat kiválasztva! Kérem jelölje meg melyik feladatot szeretné szerkeszteni!"
+ notice_account_pending: "A fiókja létrejött, és adminisztrátori jóváhagyásra vár."
+ notice_default_data_loaded: Az alapértelmezett konfiguráció betöltése sikeresen megtörtént.
+
+ error_can_t_load_default_data: "Az alapértelmezett konfiguráció betöltése nem lehetséges: {{value}}"
+ error_scm_not_found: "A bejegyzés, vagy revízió nem található a tárolóban."
+ error_scm_command_failed: "A tároló elérése közben hiba lépett fel: {{value}}"
+ error_scm_annotate: "A bejegyzés nem létezik, vagy nics jegyzetekkel ellátva."
+ error_issue_not_found_in_project: 'A feladat nem található, vagy nem ehhez a projekthez tartozik'
+
+ mail_subject_lost_password: Az Ön Redmine jelszava
+ mail_body_lost_password: 'A Redmine jelszó megváltoztatásához, kattintson a következő linkre:'
+ mail_subject_register: Redmine azonosító aktiválása
+ mail_body_register: 'A Redmine azonosítója aktiválásához, kattintson a következő linkre:'
+ mail_body_account_information_external: "A {{value}} azonosító használatával bejelentkezhet a Redmineba."
+ mail_body_account_information: Az Ön Redmine azonosítójának információi
+ mail_subject_account_activation_request: Redmine azonosító aktiválási kérelem
+ mail_body_account_activation_request: "Egy új felhasználó ({{value}}) regisztrált, azonosítója jóváhasgyásra várakozik:"
+
+ gui_validation_error: 1 hiba
+ gui_validation_error_plural: "{{count}} hiba"
+
+ field_name: Név
+ field_description: Leírás
+ field_summary: Összegzés
+ field_is_required: Kötelező
+ field_firstname: Keresztnév
+ field_lastname: Vezetéknév
+ field_mail: E-mail
+ field_filename: Fájl
+ field_filesize: Méret
+ field_downloads: Letöltések
+ field_author: Szerző
+ field_created_on: Létrehozva
+ field_updated_on: Módosítva
+ field_field_format: Formátum
+ field_is_for_all: Minden projekthez
+ field_possible_values: Lehetséges értékek
+ field_regexp: Reguláris kifejezés
+ field_min_length: Minimum hossz
+ field_max_length: Maximum hossz
+ field_value: Érték
+ field_category: Kategória
+ field_title: Cím
+ field_project: Projekt
+ field_issue: Feladat
+ field_status: Státusz
+ field_notes: Feljegyzések
+ field_is_closed: Feladat lezárva
+ field_is_default: Alapértelmezett érték
+ field_tracker: Típus
+ field_subject: Tárgy
+ field_due_date: Befejezés dátuma
+ field_assigned_to: Felelős
+ field_priority: Prioritás
+ field_fixed_version: Cél verzió
+ field_user: Felhasználó
+ field_role: Szerepkör
+ field_homepage: Weboldal
+ field_is_public: Nyilvános
+ field_parent: Szülő projekt
+ field_is_in_chlog: Feladatok látszanak a változás naplóban
+ field_is_in_roadmap: Feladatok látszanak az életútban
+ field_login: Azonosító
+ field_mail_notification: E-mail értesítések
+ field_admin: Adminisztrátor
+ field_last_login_on: Utolsó bejelentkezés
+ field_language: Nyelv
+ field_effective_date: Dátum
+ field_password: Jelszó
+ field_new_password: Új jelszó
+ field_password_confirmation: Megerősítés
+ field_version: Verzió
+ field_type: Típus
+ field_host: Kiszolgáló
+ field_port: Port
+ field_account: Felhasználói fiók
+ field_base_dn: Base DN
+ field_attr_login: Bejelentkezési tulajdonság
+ field_attr_firstname: Családnév
+ field_attr_lastname: Utónév
+ field_attr_mail: E-mail
+ field_onthefly: On-the-fly felhasználó létrehozás
+ field_start_date: Kezdés dátuma
+ field_done_ratio: Elkészült (%)
+ field_auth_source: Azonosítási mód
+ field_hide_mail: Rejtse el az e-mail címem
+ field_comments: Megjegyzés
+ field_url: URL
+ field_start_page: Kezdőlap
+ field_subproject: Alprojekt
+ field_hours: Óra
+ field_activity: Aktivitás
+ field_spent_on: Dátum
+ field_identifier: Azonosító
+ field_is_filter: Szűrőként használható
+ field_issue_to: Kapcsolódó feladat
+ field_delay: Késés
+ field_assignable: Feladat rendelhető ehhez a szerepkörhöz
+ field_redirect_existing_links: Létező linkek átirányítása
+ field_estimated_hours: Becsült idő
+ field_column_names: Oszlopok
+ field_time_zone: Időzóna
+ field_searchable: Kereshető
+ field_default_value: Alapértelmezett érték
+ field_comments_sorting: Feljegyzések megjelenítése
+
+ setting_app_title: Alkalmazás címe
+ setting_app_subtitle: Alkalmazás alcíme
+ setting_welcome_text: Üdvözlő üzenet
+ setting_default_language: Alapértelmezett nyelv
+ setting_login_required: Azonosítás szükséges
+ setting_self_registration: Regisztráció
+ setting_attachment_max_size: Melléklet max. mérete
+ setting_issues_export_limit: Feladatok exportálásának korlátja
+ setting_mail_from: Kibocsátó e-mail címe
+ setting_bcc_recipients: Titkos másolat címzet (bcc)
+ setting_host_name: Kiszolgáló neve
+ setting_text_formatting: Szöveg formázás
+ setting_wiki_compression: Wiki történet tömörítés
+ setting_feeds_limit: RSS tartalom korlát
+ setting_default_projects_public: Az új projektek alapértelmezés szerint nyilvánosak
+ setting_autofetch_changesets: Commitok automatikus lehúzása
+ setting_sys_api_enabled: WS engedélyezése a tárolók kezeléséhez
+ setting_commit_ref_keywords: Hivatkozó kulcsszavak
+ setting_commit_fix_keywords: Javítások kulcsszavai
+ setting_autologin: Automatikus bejelentkezés
+ setting_date_format: Dátum formátum
+ setting_time_format: Idő formátum
+ setting_cross_project_issue_relations: Kereszt-projekt feladat hivatkozások engedélyezése
+ setting_issue_list_default_columns: Az alapértelmezésként megjelenített oszlopok a feladat listában
+ setting_repositories_encodings: Tárolók kódolása
+ setting_emails_footer: E-mail lábléc
+ setting_protocol: Protokol
+ setting_per_page_options: Objektum / oldal opciók
+ setting_user_format: Felhasználók megjelenítésének formája
+ setting_activity_days_default: Napok megjelenítése a project aktivitásnál
+ setting_display_subprojects_issues: Alapértelmezettként mutassa az alprojektek feladatait is a projekteken
+
+ project_module_issue_tracking: Feladat követés
+ project_module_time_tracking: Idő rögzítés
+ project_module_news: Hírek
+ project_module_documents: Dokumentumok
+ project_module_files: Fájlok
+ project_module_wiki: Wiki
+ project_module_repository: Tároló
+ project_module_boards: Fórumok
+
+ label_user: Felhasználó
+ label_user_plural: Felhasználók
+ label_user_new: Új felhasználó
+ label_project: Projekt
+ label_project_new: Új projekt
+ label_project_plural: Projektek
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Az összes projekt
+ label_project_latest: Legutóbbi projektek
+ label_issue: Feladat
+ label_issue_new: Új feladat
+ label_issue_plural: Feladatok
+ label_issue_view_all: Minden feladat megtekintése
+ label_issues_by: "{{value}} feladatai"
+ label_issue_added: Feladat hozzáadva
+ label_issue_updated: Feladat frissítve
+ label_document: Dokumentum
+ label_document_new: Új dokumentum
+ label_document_plural: Dokumentumok
+ label_document_added: Dokumentum hozzáadva
+ label_role: Szerepkör
+ label_role_plural: Szerepkörök
+ label_role_new: Új szerepkör
+ label_role_and_permissions: Szerepkörök, és jogosultságok
+ label_member: Résztvevő
+ label_member_new: Új résztvevő
+ label_member_plural: Résztvevők
+ label_tracker: Feladat típus
+ label_tracker_plural: Feladat típusok
+ label_tracker_new: Új feladat típus
+ label_workflow: Workflow
+ label_issue_status: Feladat státusz
+ label_issue_status_plural: Feladat státuszok
+ label_issue_status_new: Új státusz
+ label_issue_category: Feladat kategória
+ label_issue_category_plural: Feladat kategóriák
+ label_issue_category_new: Új kategória
+ label_custom_field: Egyéni mező
+ label_custom_field_plural: Egyéni mezők
+ label_custom_field_new: Új egyéni mező
+ label_enumerations: Felsorolások
+ label_enumeration_new: Új érték
+ label_information: Információ
+ label_information_plural: Információk
+ label_please_login: Jelentkezzen be
+ label_register: Regisztráljon
+ label_password_lost: Elfelejtett jelszó
+ label_home: Kezdőlap
+ label_my_page: Saját kezdőlapom
+ label_my_account: Fiókom adatai
+ label_my_projects: Saját projektem
+ label_administration: Adminisztráció
+ label_login: Bejelentkezés
+ label_logout: Kijelentkezés
+ label_help: Súgó
+ label_reported_issues: Bejelentett feladatok
+ label_assigned_to_me_issues: A nekem kiosztott feladatok
+ label_last_login: Utolsó bejelentkezés
+ label_registered_on: Regisztrált
+ label_activity: Tevékenységek
+ label_overall_activity: Teljes aktivitás
+ label_new: Új
+ label_logged_as: Bejelentkezve, mint
+ label_environment: Környezet
+ label_authentication: Azonosítás
+ label_auth_source: Azonosítás módja
+ label_auth_source_new: Új azonosítási mód
+ label_auth_source_plural: Azonosítási módok
+ label_subproject_plural: Alprojektek
+ label_and_its_subprojects: "{{value}} és alprojektjei"
+ label_min_max_length: Min - Max hossz
+ label_list: Lista
+ label_date: Dátum
+ label_integer: Egész
+ label_float: Lebegőpontos
+ label_boolean: Logikai
+ label_string: Szöveg
+ label_text: Hosszú szöveg
+ label_attribute: Tulajdonság
+ label_attribute_plural: Tulajdonságok
+ label_download: "{{count}} Letöltés"
+ label_download_plural: "{{count}} Letöltések"
+ label_no_data: Nincs megjeleníthető adat
+ label_change_status: Státusz módosítása
+ label_history: Történet
+ label_attachment: Fájl
+ label_attachment_new: Új fájl
+ label_attachment_delete: Fájl törlése
+ label_attachment_plural: Fájlok
+ label_file_added: Fájl hozzáadva
+ label_report: Jelentés
+ label_report_plural: Jelentések
+ label_news: Hírek
+ label_news_new: Hír hozzáadása
+ label_news_plural: Hírek
+ label_news_latest: Legutóbbi hírek
+ label_news_view_all: Minden hír megtekintése
+ label_news_added: Hír hozzáadva
+ label_change_log: Változás napló
+ label_settings: Beállítások
+ label_overview: Áttekintés
+ label_version: Verzió
+ label_version_new: Új verzió
+ label_version_plural: Verziók
+ label_confirmation: Jóváhagyás
+ label_export_to: Exportálás
+ label_read: Olvas...
+ label_public_projects: Nyilvános projektek
+ label_open_issues: nyitott
+ label_open_issues_plural: nyitott
+ label_closed_issues: lezárt
+ label_closed_issues_plural: lezárt
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Összesen
+ label_permissions: Jogosultságok
+ label_current_status: Jelenlegi státusz
+ label_new_statuses_allowed: Státusz változtatások engedélyei
+ label_all: mind
+ label_none: nincs
+ label_nobody: senki
+ label_next: Következő
+ label_previous: Előző
+ label_used_by: Használja
+ label_details: Részletek
+ label_add_note: Jegyzet hozzáadása
+ label_per_page: Oldalanként
+ label_calendar: Naptár
+ label_months_from: hónap, kezdve
+ label_gantt: Gantt
+ label_internal: Belső
+ label_last_changes: "utolsó {{count}} változás"
+ label_change_view_all: Minden változás megtekintése
+ label_personalize_page: Az oldal testreszabása
+ label_comment: Megjegyzés
+ label_comment_plural: Megjegyzés
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Megjegyzés hozzáadása
+ label_comment_added: Megjegyzés hozzáadva
+ label_comment_delete: Megjegyzések törlése
+ label_query: Egyéni lekérdezés
+ label_query_plural: Egyéni lekérdezések
+ label_query_new: Új lekérdezés
+ label_filter_add: Szűrő hozzáadása
+ label_filter_plural: Szűrők
+ label_equals: egyenlő
+ label_not_equals: nem egyenlő
+ label_in_less_than: kevesebb, mint
+ label_in_more_than: több, mint
+ label_in: in
+ label_today: ma
+ label_all_time: mindenkor
+ label_yesterday: tegnap
+ label_this_week: aktuális hét
+ label_last_week: múlt hét
+ label_last_n_days: "az elmúlt {{count}} nap"
+ label_this_month: aktuális hónap
+ label_last_month: múlt hónap
+ label_this_year: aktuális év
+ label_date_range: Dátum intervallum
+ label_less_than_ago: kevesebb, mint nappal ezelőtt
+ label_more_than_ago: több, mint nappal ezelőtt
+ label_ago: nappal ezelőtt
+ label_contains: tartalmazza
+ label_not_contains: nem tartalmazza
+ label_day_plural: nap
+ label_repository: Tároló
+ label_repository_plural: Tárolók
+ label_browse: Tallóz
+ label_modification: "{{count}} változás"
+ label_modification_plural: "{{count}} változások"
+ label_revision: Revízió
+ label_revision_plural: Revíziók
+ label_associated_revisions: Kapcsolt revíziók
+ label_added: hozzáadva
+ label_modified: módosítva
+ label_deleted: törölve
+ label_latest_revision: Legutolsó revízió
+ label_latest_revision_plural: Legutolsó revíziók
+ label_view_revisions: Revíziók megtekintése
+ label_max_size: Maximális méret
+ label_sort_highest: Az elejére
+ label_sort_higher: Eggyel feljebb
+ label_sort_lower: Eggyel lejjebb
+ label_sort_lowest: Az aljára
+ label_roadmap: Életút
+ label_roadmap_due_in: "Elkészültéig várhatóan még {{value}}"
+ label_roadmap_overdue: "{{value}} késésben"
+ label_roadmap_no_issues: Nincsenek feladatok ehhez a verzióhoz
+ label_search: Keresés
+ label_result_plural: Találatok
+ label_all_words: Minden szó
+ label_wiki: Wiki
+ label_wiki_edit: Wiki szerkesztés
+ label_wiki_edit_plural: Wiki szerkesztések
+ label_wiki_page: Wiki oldal
+ label_wiki_page_plural: Wiki oldalak
+ label_index_by_title: Cím szerint indexelve
+ label_index_by_date: Dátum szerint indexelve
+ label_current_version: Jelenlegi verzió
+ label_preview: Előnézet
+ label_feed_plural: Visszajelzések
+ label_changes_details: Változások részletei
+ label_issue_tracking: Feladat követés
+ label_spent_time: Ráfordított idő
+ label_f_hour: "{{value}} óra"
+ label_f_hour_plural: "{{value}} óra"
+ label_time_tracking: Idő követés
+ label_change_plural: Változások
+ label_statistics: Statisztikák
+ label_commits_per_month: Commits havonta
+ label_commits_per_author: Commits szerzőnként
+ label_view_diff: Különbségek megtekintése
+ label_diff_inline: soronként
+ label_diff_side_by_side: egymás mellett
+ label_options: Opciók
+ label_copy_workflow_from: Workflow másolása innen
+ label_permissions_report: Jogosultsági riport
+ label_watched_issues: Megfigyelt feladatok
+ label_related_issues: Kapcsolódó feladatok
+ label_applied_status: Alkalmazandó státusz
+ label_loading: Betöltés...
+ label_relation_new: Új kapcsolat
+ label_relation_delete: Kapcsolat törlése
+ label_relates_to: kapcsolódik
+ label_duplicates: duplikálja
+ label_blocks: zárolja
+ label_blocked_by: zárolta
+ label_precedes: megelőzi
+ label_follows: követi
+ label_end_to_start: végétől indulásig
+ label_end_to_end: végétől végéig
+ label_start_to_start: indulástól indulásig
+ label_start_to_end: indulástól végéig
+ label_stay_logged_in: Emlékezzen rám
+ label_disabled: kikapcsolva
+ label_show_completed_versions: A kész verziók mutatása
+ label_me: én
+ label_board: Fórum
+ label_board_new: Új fórum
+ label_board_plural: Fórumok
+ label_topic_plural: Témák
+ label_message_plural: Üzenetek
+ label_message_last: Utolsó üzenet
+ label_message_new: Új üzenet
+ label_message_posted: Üzenet hozzáadva
+ label_reply_plural: Válaszok
+ label_send_information: Fiók infomációk küldése a felhasználónak
+ label_year: Év
+ label_month: Hónap
+ label_week: Hét
+ label_date_from: 'Kezdet:'
+ label_date_to: 'Vége:'
+ label_language_based: A felhasználó nyelve alapján
+ label_sort_by: "{{value}} szerint rendezve"
+ label_send_test_email: Teszt e-mail küldése
+ label_feeds_access_key_created_on: "RSS hozzáférési kulcs létrehozva ennyivel ezelőtt: {{value}}"
+ label_module_plural: Modulok
+ label_added_time_by: "{{author}} adta hozzá ennyivel ezelőtt: {{age}}"
+ label_updated_time: "Utolsó módosítás ennyivel ezelőtt: {{value}}"
+ label_jump_to_a_project: Ugrás projekthez...
+ label_file_plural: Fájlok
+ label_changeset_plural: Changesets
+ label_default_columns: Alapértelmezett oszlopok
+ label_no_change_option: (Nincs változás)
+ label_bulk_edit_selected_issues: A kiválasztott feladatok kötegelt szerkesztése
+ label_theme: Téma
+ label_default: Alapértelmezett
+ label_search_titles_only: Keresés csak a címekben
+ label_user_mail_option_all: "Minden eseményről minden saját projektemben"
+ label_user_mail_option_selected: "Minden eseményről a kiválasztott projektekben..."
+ label_user_mail_option_none: "Csak a megfigyelt dolgokról, vagy, amiben részt veszek"
+ label_user_mail_no_self_notified: "Nem kérek értesítést az általam végzett módosításokról"
+ label_registration_activation_by_email: Fiók aktiválása e-mailben
+ label_registration_manual_activation: Manuális fiók aktiválás
+ label_registration_automatic_activation: Automatikus fiók aktiválás
+ label_display_per_page: "Oldalanként: {{value}}"
+ label_age: Kor
+ label_change_properties: Tulajdonságok változtatása
+ label_general: Általános
+ label_more: továbbiak
+ label_scm: SCM
+ label_plugins: Pluginek
+ label_ldap_authentication: LDAP azonosítás
+ label_downloads_abbr: D/L
+ label_optional_description: Opcionális leírás
+ label_add_another_file: Újabb fájl hozzáadása
+ label_preferences: Tulajdonságok
+ label_chronological_order: Időrendben
+ label_reverse_chronological_order: Fordított időrendben
+ label_planning: Tervezés
+
+ button_login: Bejelentkezés
+ button_submit: Elfogad
+ button_save: Mentés
+ button_check_all: Mindent kijelöl
+ button_uncheck_all: Kijelölés törlése
+ button_delete: Töröl
+ button_create: Létrehoz
+ button_test: Teszt
+ button_edit: Szerkeszt
+ button_add: Hozzáad
+ button_change: Változtat
+ button_apply: Alkalmaz
+ button_clear: Töröl
+ button_lock: Zárol
+ button_unlock: Felold
+ button_download: Letöltés
+ button_list: Lista
+ button_view: Megnéz
+ button_move: Mozgat
+ button_back: Vissza
+ button_cancel: Mégse
+ button_activate: Aktivál
+ button_sort: Rendezés
+ button_log_time: Idő rögzítés
+ button_rollback: Visszaáll erre a verzióra
+ button_watch: Megfigyel
+ button_unwatch: Megfigyelés törlése
+ button_reply: Válasz
+ button_archive: Archivál
+ button_unarchive: Dearchivál
+ button_reset: Reset
+ button_rename: Átnevez
+ button_change_password: Jelszó megváltoztatása
+ button_copy: Másol
+ button_annotate: Jegyzetel
+ button_update: Módosít
+ button_configure: Konfigurál
+
+ status_active: aktív
+ status_registered: regisztrált
+ status_locked: zárolt
+
+ text_select_mail_notifications: Válasszon eseményeket, amelyekről e-mail értesítést kell küldeni.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 = nincs korlátozás
+ text_project_destroy_confirmation: Biztosan törölni szeretné a projektet és vele együtt minden kapcsolódó adatot ?
+ text_subprojects_destroy_warning: "Az alprojekt(ek): {{value}} szintén törlésre kerülnek."
+ text_workflow_edit: Válasszon egy szerepkört, és egy trackert a workflow szerkesztéséhez
+ text_are_you_sure: Biztos benne ?
+ text_tip_task_begin_day: a feladat ezen a napon kezdődik
+ text_tip_task_end_day: a feladat ezen a napon ér véget
+ text_tip_task_begin_end_day: a feladat ezen a napon kezdődik és ér véget
+ text_project_identifier_info: 'Kis betűk (a-z), számok és kötőjel megengedett.<br />Mentés után az azonosítót megváltoztatni nem lehet.'
+ text_caracters_maximum: "maximum {{count}} karakter."
+ text_caracters_minimum: "Legkevesebb {{count}} karakter hosszúnek kell lennie."
+ text_length_between: "Legalább {{min}} és legfeljebb {{max}} hosszú karakter."
+ text_tracker_no_workflow: Nincs workflow definiálva ehhez a tracker-hez
+ text_unallowed_characters: Tiltott karakterek
+ text_comma_separated: Több érték megengedett (vesszővel elválasztva)
+ text_issues_ref_in_commit_messages: Hivatkozás feladatokra, feladatok javítása a commit üzenetekben
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{author}}."
+ text_wiki_destroy_confirmation: Biztosan törölni szeretné ezt a wiki-t minden tartalmával együtt ?
+ text_issue_category_destroy_question: "Néhány feladat ({{count}}) hozzá van rendelve ehhez a kategóriához. Mit szeretne tenni ?"
+ text_issue_category_destroy_assignments: Kategória hozzárendelés megszűntetése
+ text_issue_category_reassign_to: Feladatok újra hozzárendelése a kategóriához
+ text_user_mail_option: "A nem kiválasztott projektekről csak akkor kap értesítést, ha figyelést kér rá, vagy részt vesz benne (pl. Ön a létrehozó, vagy a hozzárendelő)"
+ text_no_configuration_data: "Szerepkörök, trackerek, feladat státuszok, és workflow adatok még nincsenek konfigurálva.\nErősen ajánlott, az alapértelmezett konfiguráció betöltése, és utána módosíthatja azt."
+ text_load_default_configuration: Alapértelmezett konfiguráció betöltése
+ text_status_changed_by_changeset: "Applied in changeset {{value}}."
+ text_issues_destroy_confirmation: 'Biztos benne, hogy törölni szeretné a kijelölt feladato(ka)t ?'
+ text_select_project_modules: 'Válassza ki az engedélyezett modulokat ehhez a projekthez:'
+ text_default_administrator_account_changed: Alapértelmezett adminisztrátor fiók megváltoztatva
+ text_file_repository_writable: Fájl tároló írható
+ text_rmagick_available: RMagick elérhető (opcionális)
+ text_destroy_time_entries_question: "{{hours}} órányi munka van rögzítve a feladatokon, amiket törölni szeretne. Mit szeretne tenni ?"
+ text_destroy_time_entries: A rögzített órák törlése
+ text_assign_time_entries_to_project: A rögzített órák hozzárendelése a projekthez
+ text_reassign_time_entries: 'A rögzített órák újra hozzárendelése ehhez a feladathoz:'
+
+ default_role_manager: Vezető
+ default_role_developper: Fejlesztő
+ default_role_reporter: Bejelentő
+ default_tracker_bug: Hiba
+ default_tracker_feature: Fejlesztés
+ default_tracker_support: Support
+ default_issue_status_new: Új
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Megoldva
+ default_issue_status_feedback: Visszajelzés
+ default_issue_status_closed: Lezárt
+ default_issue_status_rejected: Elutasított
+ default_doc_category_user: Felhasználói dokumentáció
+ default_doc_category_tech: Technikai dokumentáció
+ default_priority_low: Alacsony
+ default_priority_normal: Normál
+ default_priority_high: Magas
+ default_priority_urgent: Sürgős
+ default_priority_immediate: Azonnal
+ default_activity_design: Tervezés
+ default_activity_development: Fejlesztés
+
+ enumeration_issue_priorities: Feladat prioritások
+ enumeration_doc_categories: Dokumentum kategóriák
+ enumeration_activities: Tevékenységek (idő rögzítés)
+ mail_body_reminder: "{{count}} neked kiosztott feladat határidős az elkövetkező {{days}} napban:"
+ mail_subject_reminder: "{{count}} feladat határidős az elkövetkező napokban"
+ text_user_wrote: "{{value}} írta:"
+ label_duplicated_by: duplikálta
+ setting_enabled_scm: Forráskódkezelő (SCM) engedélyezése
+ text_enumeration_category_reassign_to: 'Újra hozzárendelés ehhez:'
+ text_enumeration_destroy_question: "{{count}} objektum van hozzárendelve ehhez az értékhez."
+ label_incoming_emails: Beérkezett levelek
+ label_generate_key: Kulcs generálása
+ setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez
+ setting_mail_handler_api_key: API kulcs
+ text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÁllítsd be az SMTP szervert a config/email.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen."
+ field_parent_title: Szülő oldal
+ label_issue_watchers: Megfigyelők
+ setting_commit_logs_encoding: Commit üzenetek kódlapja
+ button_quote: Hozzászólás / Idézet / Kérdés
+ setting_sequential_project_identifiers: Szekvenciális projekt azonosítók generálása
+ notice_unable_delete_version: A verziót nem lehet törölni
+ label_renamed: átnevezve
+ label_copied: lemásolva
+ setting_plain_text_mail: csak szöveg (nem HTML)
+ permission_view_files: Fájlok megtekintése
+ permission_edit_issues: Feladatok szerkesztése
+ permission_edit_own_time_entries: Saját időnapló szerkesztése
+ permission_manage_public_queries: Nyilvános kérések kezelése
+ permission_add_issues: Feladat felvétele
+ permission_log_time: Idő rögzítése
+ permission_view_changesets: Változáskötegek megtekintése
+ permission_view_time_entries: Időrögzítések megtekintése
+ permission_manage_versions: Verziók kezelése
+ permission_manage_wiki: Wiki kezelése
+ permission_manage_categories: Feladat kategóriák kezelése
+ permission_protect_wiki_pages: Wiki oldalak védelme
+ permission_comment_news: Hírek kommentelése
+ permission_delete_messages: Üzenetek törlése
+ permission_select_project_modules: Projekt modulok kezelése
+ permission_manage_documents: Dokumentumok kezelése
+ permission_edit_wiki_pages: Wiki oldalak szerkesztése
+ permission_add_issue_watchers: Megfigyelők felvétele
+ permission_view_gantt: Gannt diagramm megtekintése
+ permission_move_issues: Feladatok mozgatása
+ permission_manage_issue_relations: Feladat kapcsolatok kezelése
+ permission_delete_wiki_pages: Wiki oldalak törlése
+ permission_manage_boards: Fórumok kezelése
+ permission_delete_wiki_pages_attachments: Csatolmányok törlése
+ permission_view_wiki_edits: Wiki történet megtekintése
+ permission_add_messages: Üzenet beküldése
+ permission_view_messages: Üzenetek megtekintése
+ permission_manage_files: Fájlok kezelése
+ permission_edit_issue_notes: Jegyzetek szerkesztése
+ permission_manage_news: Hírek kezelése
+ permission_view_calendar: Naptár megtekintése
+ permission_manage_members: Tagok kezelése
+ permission_edit_messages: Üzenetek szerkesztése
+ permission_delete_issues: Feladatok törlése
+ permission_view_issue_watchers: Megfigyelők listázása
+ permission_manage_repository: Tárolók kezelése
+ permission_commit_access: Commit hozzáférés
+ permission_browse_repository: Tároló böngészése
+ permission_view_documents: Dokumetumok megtekintése
+ permission_edit_project: Projekt szerkesztése
+ permission_add_issue_notes: Jegyzet rögzítése
+ permission_save_queries: Kérések mentése
+ permission_view_wiki_pages: Wiki megtekintése
+ permission_rename_wiki_pages: Wiki oldalak átnevezése
+ permission_edit_time_entries: Időnaplók szerkesztése
+ permission_edit_own_issue_notes: Saját jegyzetek szerkesztése
+ setting_gravatar_enabled: Felhasználói fényképek engedélyezése
+ label_example: Példa
+ text_repository_usernames_mapping: "Állítsd be a felhasználó összerendeléseket a Redmine, és a tároló logban található felhasználók között.\nAz azonos felhasználó nevek összerendelése automatikusan megtörténik."
+ permission_edit_own_messages: Saját üzenetek szerkesztése
+ permission_delete_own_messages: Saját üzenetek törlése
+ label_user_activity: "{{value}} tevékenységei"
+ label_updated_time_by: "Módosította {{author}} ennyivel ezelőtt: {{age}}"
+ text_diff_truncated: '... A diff fájl vége nem jelenik meg, mert hosszab, mint a megjeleníthető sorok száma.'
+ setting_diff_max_lines_displayed: A megjelenítendő sorok száma (maximum) a diff fájloknál
+ text_plugin_assets_writable: Plugin eszközök könyvtár írható
+ warning_attachments_not_saved: "{{count}} fájl mentése nem sikerült."
+ button_create_and_continue: Létrehozás és folytatás
+ text_custom_field_possible_values_info: 'Értékenként egy sor'
+ label_display: Megmutat
+ field_editable: Szerkeszthető
+ setting_repository_log_display_limit: Maximum hány revíziót mutasson meg a log megjelenítésekor
+ setting_file_max_size_displayed: Maximum mekkora szövegfájlokat jelenítsen meg soronkénti összehasonlításnál
+ field_watcher: Megfigyelő
+ setting_openid: OpenID regisztráció és bejelentkezés engedélyezése
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: bejelentkezés OpenID használatával
+ field_content: Tartalom
+ label_descending: Csökkenő
+ label_sort: Rendezés
+ label_ascending: Növekvő
+ label_date_from_to: "{{start}} -tól {{end}} -ig"
+ label_greater_or_equal: ">="
+ label_less_or_equal: "<="
+ text_wiki_page_destroy_question: Ennek az oldalnak {{descendants}} gyermek-, és leszármazott oldala van. Mit szeretne tenni?
+ text_wiki_page_reassign_children: Az aloldalak hozzárendelése ehhez a szülő oldalhoz
+ text_wiki_page_nullify_children: Az aloldalak megtartása, mint főoldalak
+ text_wiki_page_destroy_children: Minden aloldal és leszármazottjának törlése
+ setting_password_min_length: Minimum jelszó hosszúság
+ field_group_by: Szerint csoportosítva
+ mail_subject_wiki_content_updated: "'{{page}}' wiki oldal frissítve"
+ label_wiki_content_added: Wiki oldal hozzáadve
+ mail_subject_wiki_content_added: "Új wiki oldal: '{{page}}'"
+ mail_body_wiki_content_added: A '{{page}}' wiki oldalt {{author}} hozta létre.
+ label_wiki_content_updated: Wiki oldal frissítve
+ mail_body_wiki_content_updated: A '{{page}}' wiki oldalt {{author}} frissítette.
+ permission_add_project: Projekt létrehozása
+ setting_new_project_user_role_id: Projekt létrehozási jog nem adminisztrátor felhasználóknak
+ label_view_all_revisions: 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
--- /dev/null
+# Italian translations for Ruby on Rails
+# by Claudio Poli (masterkain@gmail.com)
+
+it:
+ date:
+ formats:
+ default: "%d-%m-%Y"
+ short: "%d %b"
+ long: "%d %B %Y"
+ only_day: "%e"
+
+ day_names: [Domenica, Lunedì, Martedì, Mercoledì, Giovedì, Venerdì, Sabato]
+ abbr_day_names: [Dom, Lun, Mar, Mer, Gio, Ven, Sab]
+ month_names: [~, Gennaio, Febbraio, Marzo, Aprile, Maggio, Giugno, Luglio, Agosto, Settembre, Ottobre, Novembre, Dicembre]
+ abbr_month_names: [~, Gen, Feb, Mar, Apr, Mag, Giu, Lug, Ago, Set, Ott, Nov, Dic]
+ 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"
+ only_second: "%S"
+
+ datetime:
+ formats:
+ default: "%d-%m-%YT%H:%M:%S%Z"
+
+ am: 'am'
+ pm: 'pm'
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "mezzo minuto"
+ less_than_x_seconds:
+ one: "meno di un secondo"
+ other: "meno di {{count}} secondi"
+ x_seconds:
+ one: "1 secondo"
+ other: "{{count}} secondi"
+ less_than_x_minutes:
+ one: "meno di un minuto"
+ other: "meno di {{count}} minuti"
+ x_minutes:
+ one: "1 minuto"
+ other: "{{count}} minuti"
+ about_x_hours:
+ one: "circa un'ora"
+ other: "circa {{count}} ore"
+ x_days:
+ one: "1 giorno"
+ other: "{{count}} giorni"
+ about_x_months:
+ one: "circa un mese"
+ other: "circa {{count}} mesi"
+ x_months:
+ one: "1 mese"
+ other: "{{count}} mesi"
+ about_x_years:
+ one: "circa un anno"
+ other: "circa {{count}} anni"
+ over_x_years:
+ one: "oltre un anno"
+ other: "oltre {{count}} anni"
+
+ number:
+ format:
+ precision: 3
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: '€'
+ precision: 2
+ format: '%n %u'
+ human:
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Non posso salvare questo {{model}}: 1 errore"
+ other: "Non posso salvare questo {{model}}: {{count}} errori."
+ body: "Per favore ricontrolla i seguenti campi:"
+ messages:
+ inclusion: "non è incluso nella lista"
+ exclusion: "è riservato"
+ invalid: "non è valido"
+ confirmation: "non coincide con la conferma"
+ accepted: "deve essere accettata"
+ empty: "non può essere vuoto"
+ blank: "non può essere lasciato in bianco"
+ too_long: "è troppo lungo (il massimo è {{count}} lettere)"
+ too_short: "è troppo corto (il minimo è {{count}} lettere)"
+ wrong_length: "è della lunghezza sbagliata (deve essere di {{count}} lettere)"
+ taken: "è già in uso"
+ not_a_number: "non è un numero"
+ greater_than: "deve essere superiore a {{count}}"
+ greater_than_or_equal_to: "deve essere superiore o uguale a {{count}}"
+ equal_to: "deve essere uguale a {{count}}"
+ less_than: "deve essere meno di {{count}}"
+ less_than_or_equal_to: "deve essere meno o uguale a {{count}}"
+ odd: "deve essere dispari"
+ even: "deve essere pari"
+ greater_than_start_date: "deve essere maggiore della data di partenza"
+ not_same_project: "non appartiene allo stesso progetto"
+ circular_dependency: "Questa relazione creerebbe una dipendenza circolare"
+
+ actionview_instancetag_blank_option: Scegli
+
+ general_text_No: 'No'
+ general_text_Yes: 'Si'
+ general_text_no: 'no'
+ general_text_yes: 'si'
+ general_lang_name: 'Italiano'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: L'utenza è stata aggiornata.
+ notice_account_invalid_creditentials: Nome utente o password non validi.
+ notice_account_password_updated: La password è stata aggiornata.
+ notice_account_wrong_password: Password errata
+ notice_account_register_done: L'utenza è stata creata.
+ notice_account_unknown_email: Utente sconosciuto.
+ notice_can_t_change_password: Questa utenza utilizza un metodo di autenticazione esterno. Impossibile cambiare la password.
+ notice_account_lost_email_sent: Ti è stata spedita una email con le istruzioni per cambiare la password.
+ notice_account_activated: Il tuo account è stato attivato. Ora puoi effettuare l'accesso.
+ notice_successful_create: Creazione effettuata.
+ notice_successful_update: Modifica effettuata.
+ notice_successful_delete: Eliminazione effettuata.
+ notice_successful_connection: Connessione effettuata.
+ notice_file_not_found: La pagina desiderata non esiste o è stata rimossa.
+ notice_locking_conflict: Le informazioni sono state modificate da un altro utente.
+ notice_not_authorized: Non sei autorizzato ad accedere a questa pagina.
+ notice_email_sent: "Una e-mail è stata spedita a {{value}}"
+ notice_email_error: "Si è verificato un errore durante l'invio di una e-mail ({{value}})"
+ notice_feeds_access_key_reseted: La tua chiave di accesso RSS è stata reimpostata.
+
+ error_scm_not_found: "La risorsa e/o la versione non esistono nel repository."
+ error_scm_command_failed: "Si è verificato un errore durante l'accesso al repository: {{value}}"
+
+ mail_subject_lost_password: "Password {{value}}"
+ mail_body_lost_password: 'Per cambiare la password, usate il seguente collegamento:'
+ mail_subject_register: "Attivazione utenza {{value}}"
+ mail_body_register: 'Per attivare la vostra utenza, usate il seguente collegamento:'
+
+ gui_validation_error: 1 errore
+ gui_validation_error_plural: "{{count}} errori"
+
+ field_name: Nome
+ field_description: Descrizione
+ field_summary: Sommario
+ field_is_required: Richiesto
+ field_firstname: Nome
+ field_lastname: Cognome
+ field_mail: Email
+ field_filename: File
+ field_filesize: Dimensione
+ field_downloads: Download
+ field_author: Autore
+ field_created_on: Creato
+ field_updated_on: Aggiornato
+ field_field_format: Formato
+ field_is_for_all: Per tutti i progetti
+ field_possible_values: Valori possibili
+ field_regexp: Espressione regolare
+ field_min_length: Lunghezza minima
+ field_max_length: Lunghezza massima
+ field_value: Valore
+ field_category: Categoria
+ field_title: Titolo
+ field_project: Progetto
+ field_issue: Segnalazione
+ field_status: Stato
+ field_notes: Note
+ field_is_closed: Chiude la segnalazione
+ field_is_default: Stato predefinito
+ field_tracker: Tracker
+ field_subject: Oggetto
+ field_due_date: Data ultima
+ field_assigned_to: Assegnato a
+ field_priority: Priorita'
+ field_fixed_version: Versione prevista
+ field_user: Utente
+ field_role: Ruolo
+ field_homepage: Homepage
+ field_is_public: Pubblico
+ field_parent: Sottoprogetto di
+ field_is_in_chlog: Segnalazioni mostrate nel changelog
+ field_is_in_roadmap: Segnalazioni mostrate nel roadmap
+ field_login: Login
+ field_mail_notification: Notifiche via e-mail
+ field_admin: Amministratore
+ field_last_login_on: Ultima connessione
+ field_language: Lingua
+ field_effective_date: Data
+ field_password: Password
+ field_new_password: Nuova password
+ field_password_confirmation: Conferma
+ field_version: Versione
+ field_type: Tipo
+ field_host: Host
+ field_port: Porta
+ field_account: Utenza
+ field_base_dn: DN base
+ field_attr_login: Attributo login
+ field_attr_firstname: Attributo nome
+ field_attr_lastname: Attributo cognome
+ field_attr_mail: Attributo e-mail
+ field_onthefly: Creazione utenza "al volo"
+ field_start_date: Inizio
+ field_done_ratio: % completato
+ field_auth_source: Modalità di autenticazione
+ field_hide_mail: Nascondi il mio indirizzo di e-mail
+ field_comments: Commento
+ field_url: URL
+ field_start_page: Pagina principale
+ field_subproject: Sottoprogetto
+ field_hours: Ore
+ field_activity: Attività
+ field_spent_on: Data
+ field_identifier: Identificativo
+ field_is_filter: Usato come filtro
+ field_issue_to: Segnalazioni correlate
+ field_delay: Ritardo
+ field_assignable: E' possibile assegnare segnalazioni a questo ruolo
+ field_redirect_existing_links: Redirige i collegamenti esistenti
+ field_estimated_hours: Tempo stimato
+ field_default_value: Stato predefinito
+
+ setting_app_title: Titolo applicazione
+ setting_app_subtitle: Sottotitolo applicazione
+ setting_welcome_text: Testo di benvenuto
+ setting_default_language: Lingua predefinita
+ setting_login_required: Autenticazione richiesta
+ setting_self_registration: Auto-registrazione abilitata
+ setting_attachment_max_size: Massima dimensione allegati
+ setting_issues_export_limit: Limite esportazione segnalazioni
+ setting_mail_from: Indirizzo sorgente e-mail
+ setting_host_name: Nome host
+ setting_text_formatting: Formattazione testo
+ setting_wiki_compression: Comprimi cronologia wiki
+ setting_feeds_limit: Limite contenuti del feed
+ setting_autofetch_changesets: Acquisisci automaticamente le commit
+ setting_sys_api_enabled: Abilita WS per la gestione del repository
+ setting_commit_ref_keywords: Parole chiave riferimento
+ setting_commit_fix_keywords: Parole chiave chiusura
+ setting_autologin: Login automatico
+ setting_date_format: Formato data
+ setting_cross_project_issue_relations: Consenti la creazione di relazioni tra segnalazioni in progetti differenti
+
+ label_user: Utente
+ label_user_plural: Utenti
+ label_user_new: Nuovo utente
+ label_project: Progetto
+ label_project_new: Nuovo progetto
+ label_project_plural: Progetti
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Tutti i progetti
+ label_project_latest: Ultimi progetti registrati
+ label_issue: Segnalazione
+ label_issue_new: Nuova segnalazione
+ label_issue_plural: Segnalazioni
+ label_issue_view_all: Mostra tutte le segnalazioni
+ label_document: Documento
+ label_document_new: Nuovo documento
+ label_document_plural: Documenti
+ label_role: Ruolo
+ label_role_plural: Ruoli
+ label_role_new: Nuovo ruolo
+ label_role_and_permissions: Ruoli e permessi
+ label_member: Membro
+ label_member_new: Nuovo membro
+ label_member_plural: Membri
+ label_tracker: Tracker
+ label_tracker_plural: Tracker
+ label_tracker_new: Nuovo tracker
+ label_workflow: Workflow
+ label_issue_status: Stato segnalazioni
+ label_issue_status_plural: Stati segnalazione
+ label_issue_status_new: Nuovo stato
+ label_issue_category: Categorie segnalazioni
+ label_issue_category_plural: Categorie segnalazioni
+ label_issue_category_new: Nuova categoria
+ label_custom_field: Campo personalizzato
+ label_custom_field_plural: Campi personalizzati
+ label_custom_field_new: Nuovo campo personalizzato
+ label_enumerations: Enumerazioni
+ label_enumeration_new: Nuovo valore
+ label_information: Informazione
+ label_information_plural: Informazioni
+ label_please_login: Autenticarsi
+ label_register: Registrati
+ label_password_lost: Password dimenticata
+ label_home: Home
+ label_my_page: Pagina personale
+ label_my_account: La mia utenza
+ label_my_projects: I miei progetti
+ label_administration: Amministrazione
+ label_login: Login
+ label_logout: Logout
+ label_help: Aiuto
+ label_reported_issues: Segnalazioni
+ label_assigned_to_me_issues: Le mie segnalazioni
+ label_last_login: Ultimo collegamento
+ label_registered_on: Registrato il
+ label_activity: Attività
+ label_new: Nuovo
+ label_logged_as: Autenticato come
+ label_environment: Ambiente
+ label_authentication: Autenticazione
+ label_auth_source: Modalità di autenticazione
+ label_auth_source_new: Nuova modalità di autenticazione
+ label_auth_source_plural: Modalità di autenticazione
+ label_subproject_plural: Sottoprogetti
+ label_min_max_length: Lunghezza minima - massima
+ label_list: Elenco
+ label_date: Data
+ label_integer: Intero
+ label_boolean: Booleano
+ label_string: Testo
+ label_text: Testo esteso
+ label_attribute: Attributo
+ label_attribute_plural: Attributi
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Download"
+ label_no_data: Nessun dato disponibile
+ label_change_status: Cambia stato
+ label_history: Cronologia
+ label_attachment: File
+ label_attachment_new: Nuovo file
+ label_attachment_delete: Elimina file
+ label_attachment_plural: File
+ label_report: Report
+ label_report_plural: Report
+ label_news: Notizia
+ label_news_new: Aggiungi notizia
+ label_news_plural: Notizie
+ label_news_latest: Utime notizie
+ label_news_view_all: Tutte le notizie
+ label_change_log: Elenco modifiche
+ label_settings: Impostazioni
+ label_overview: Panoramica
+ label_version: Versione
+ label_version_new: Nuova versione
+ label_version_plural: Versioni
+ label_confirmation: Conferma
+ label_export_to: Esporta su
+ label_read: Leggi...
+ label_public_projects: Progetti pubblici
+ label_open_issues: aperta
+ label_open_issues_plural: aperte
+ label_closed_issues: chiusa
+ label_closed_issues_plural: chiuse
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Totale
+ label_permissions: Permessi
+ label_current_status: Stato attuale
+ label_new_statuses_allowed: Nuovi stati possibili
+ label_all: tutti
+ label_none: nessuno
+ label_next: Successivo
+ label_previous: Precedente
+ label_used_by: Usato da
+ label_details: Dettagli
+ label_add_note: Aggiungi una nota
+ label_per_page: Per pagina
+ label_calendar: Calendario
+ label_months_from: mesi da
+ label_gantt: Gantt
+ label_internal: Interno
+ label_last_changes: "ultime {{count}} modifiche"
+ label_change_view_all: Tutte le modifiche
+ label_personalize_page: Personalizza la pagina
+ label_comment: Commento
+ label_comment_plural: Commenti
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Aggiungi un commento
+ label_comment_added: Commento aggiunto
+ label_comment_delete: Elimina commenti
+ label_query: Query personalizzata
+ label_query_plural: Query personalizzate
+ label_query_new: Nuova query
+ label_filter_add: Aggiungi filtro
+ label_filter_plural: Filtri
+ label_equals: è
+ label_not_equals: non è
+ label_in_less_than: è minore di
+ label_in_more_than: è maggiore di
+ label_in: in
+ label_today: oggi
+ label_this_week: questa settimana
+ label_less_than_ago: meno di giorni fa
+ label_more_than_ago: più di giorni fa
+ label_ago: giorni fa
+ label_contains: contiene
+ label_not_contains: non contiene
+ label_day_plural: giorni
+ label_repository: Repository
+ label_browse: Sfoglia
+ label_modification: "{{count}} modifica"
+ label_modification_plural: "{{count}} modifiche"
+ label_revision: Versione
+ label_revision_plural: Versioni
+ label_added: aggiunto
+ label_modified: modificato
+ label_deleted: eliminato
+ label_latest_revision: Ultima versione
+ label_latest_revision_plural: Ultime versioni
+ label_view_revisions: Mostra versioni
+ label_max_size: Dimensione massima
+ label_sort_highest: Sposta in cima
+ label_sort_higher: Su
+ label_sort_lower: Giù
+ label_sort_lowest: Sposta in fondo
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Da ultimare in {{value}}"
+ label_roadmap_overdue: "{{value}} di ritardo"
+ label_roadmap_no_issues: Nessuna segnalazione per questa versione
+ label_search: Ricerca
+ label_result_plural: Risultati
+ label_all_words: Tutte le parole
+ label_wiki: Wiki
+ label_wiki_edit: Modifica Wiki
+ label_wiki_edit_plural: Modfiche wiki
+ label_wiki_page: Pagina Wiki
+ label_wiki_page_plural: Pagine Wiki
+ label_index_by_title: Ordina per titolo
+ label_index_by_date: Ordina per data
+ label_current_version: Versione corrente
+ label_preview: Anteprima
+ label_feed_plural: Feed
+ label_changes_details: Particolari di tutti i cambiamenti
+ label_issue_tracking: Tracking delle segnalazioni
+ label_spent_time: Tempo impiegato
+ label_f_hour: "{{value}} ora"
+ label_f_hour_plural: "{{value}} ore"
+ label_time_tracking: Tracking del tempo
+ label_change_plural: Modifiche
+ label_statistics: Statistiche
+ label_commits_per_month: Commit per mese
+ label_commits_per_author: Commit per autore
+ label_view_diff: mostra differenze
+ label_diff_inline: in linea
+ label_diff_side_by_side: fianco a fianco
+ label_options: Opzioni
+ label_copy_workflow_from: Copia workflow da
+ label_permissions_report: Report permessi
+ label_watched_issues: Segnalazioni osservate
+ label_related_issues: Segnalazioni correlate
+ label_applied_status: Stato applicato
+ label_loading: Caricamento...
+ label_relation_new: Nuova relazione
+ label_relation_delete: Elimina relazione
+ label_relates_to: correlato a
+ label_duplicates: duplicati
+ label_blocks: blocchi
+ label_blocked_by: bloccato da
+ label_precedes: precede
+ label_follows: segue
+ label_end_to_start: end to start
+ label_end_to_end: end to end
+ label_start_to_start: start to start
+ label_start_to_end: start to end
+ label_stay_logged_in: Rimani collegato
+ label_disabled: disabilitato
+ label_show_completed_versions: Mostra versioni completate
+ label_me: io
+ label_board: Forum
+ label_board_new: Nuovo forum
+ label_board_plural: Forum
+ label_topic_plural: Argomenti
+ label_message_plural: Messaggi
+ label_message_last: Ultimo messaggio
+ label_message_new: Nuovo messaggio
+ label_reply_plural: Risposte
+ label_send_information: Invia all'utente le informazioni relative all'account
+ label_year: Anno
+ label_month: Mese
+ label_week: Settimana
+ label_date_from: Da
+ label_date_to: A
+ label_language_based: Basato sul linguaggio
+ label_sort_by: "Ordina per {{value}}"
+ label_send_test_email: Invia una e-mail di test
+ label_feeds_access_key_created_on: "chiave di accesso RSS creata {{value}} fa"
+ label_module_plural: Moduli
+ label_added_time_by: "Aggiunto da {{author}} {{age}} fa"
+ label_updated_time: "Aggiornato {{value}} fa"
+ label_jump_to_a_project: Vai al progetto...
+
+ button_login: Login
+ button_submit: Invia
+ button_save: Salva
+ button_check_all: Seleziona tutti
+ button_uncheck_all: Deseleziona tutti
+ button_delete: Elimina
+ button_create: Crea
+ button_test: Test
+ button_edit: Modifica
+ button_add: Aggiungi
+ button_change: Modifica
+ button_apply: Applica
+ button_clear: Pulisci
+ button_lock: Blocca
+ button_unlock: Sblocca
+ button_download: Scarica
+ button_list: Elenca
+ button_view: Mostra
+ button_move: Sposta
+ button_back: Indietro
+ button_cancel: Annulla
+ button_activate: Attiva
+ button_sort: Ordina
+ button_log_time: Registra tempo
+ button_rollback: Ripristina questa versione
+ button_watch: Osserva
+ button_unwatch: Dimentica
+ button_reply: Rispondi
+ button_archive: Archivia
+ button_unarchive: Ripristina
+ button_reset: Reset
+ button_rename: Rinomina
+
+ status_active: attivo
+ status_registered: registrato
+ status_locked: bloccato
+
+ text_select_mail_notifications: Seleziona le azioni per cui deve essere inviata una notifica.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 significa nessuna restrizione
+ text_project_destroy_confirmation: Sei sicuro di voler cancellare il progetti e tutti i dati ad esso collegati?
+ text_workflow_edit: Seleziona un ruolo ed un tracker per modificare il workflow
+ text_are_you_sure: Sei sicuro ?
+ text_tip_task_begin_day: attività che iniziano in questa giornata
+ text_tip_task_end_day: attività che terminano in questa giornata
+ text_tip_task_begin_end_day: attività che iniziano e terminano in questa giornata
+ text_project_identifier_info: "Lettere minuscole (a-z), numeri e trattini permessi.<br />Una volta salvato, l'identificativo non può essere modificato."
+ text_caracters_maximum: "massimo {{count}} caratteri."
+ text_length_between: "Lunghezza compresa tra {{min}} e {{max}} caratteri."
+ text_tracker_no_workflow: Nessun workflow definito per questo tracker
+ text_unallowed_characters: Caratteri non permessi
+ text_comma_separated: Valori multipli permessi (separati da virgola).
+ text_issues_ref_in_commit_messages: Segnalazioni di riferimento e chiusura nei messaggi di commit
+ text_issue_added: "E' stata segnalata l'anomalia {{id}} da {{author}}."
+ text_issue_updated: "L'anomalia {{id}} e' stata aggiornata da {{author}}."
+ text_wiki_destroy_confirmation: Sicuro di voler cancellare questo wiki e tutti i suoi contenuti?
+ text_issue_category_destroy_question: "Alcune segnalazioni ({{count}}) risultano assegnate a questa categoria. Cosa vuoi fare ?"
+ text_issue_category_destroy_assignments: Rimuovi gli assegnamenti a questa categoria
+ text_issue_category_reassign_to: Riassegna segnalazioni a questa categoria
+
+ default_role_manager: Manager
+ default_role_developper: Sviluppatore
+ default_role_reporter: Reporter
+ default_tracker_bug: Segnalazione
+ default_tracker_feature: Funzione
+ default_tracker_support: Supporto
+ default_issue_status_new: Nuovo
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Risolto
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Chiuso
+ default_issue_status_rejected: Rifiutato
+ default_doc_category_user: Documentazione utente
+ default_doc_category_tech: Documentazione tecnica
+ default_priority_low: Bassa
+ default_priority_normal: Normale
+ default_priority_high: Alta
+ default_priority_urgent: Urgente
+ default_priority_immediate: Immediata
+ default_activity_design: Progettazione
+ default_activity_development: Sviluppo
+
+ enumeration_issue_priorities: Priorità segnalazioni
+ enumeration_doc_categories: Categorie di documenti
+ enumeration_activities: Attività (time tracking)
+ label_file_plural: File
+ label_changeset_plural: Changeset
+ field_column_names: Colonne
+ label_default_columns: Colonne predefinite
+ setting_issue_list_default_columns: Colonne predefinite mostrate nell'elenco segnalazioni
+ setting_repositories_encodings: Codifiche dei repository
+ notice_no_issue_selected: "Nessuna segnalazione selezionata! Seleziona le segnalazioni che intendi modificare."
+ label_bulk_edit_selected_issues: Modifica massiva delle segnalazioni selezionate
+ label_no_change_option: (Nessuna modifica)
+ notice_failed_to_save_issues: "Impossibile salvare {{count}} segnalazioni su {{total}} selezionate: {{ids}}."
+ label_theme: Tema
+ label_default: Predefinito
+ label_search_titles_only: Cerca solo nei titoli
+ label_nobody: nessuno
+ button_change_password: Modifica password
+ text_user_mail_option: "Per i progetti non selezionati, riceverai solo le notifiche riguardanti le cose che osservi o nelle quali sei coinvolto (per esempio segnalazioni che hai creato o che ti sono state assegnate)."
+ label_user_mail_option_selected: "Solo per gli eventi relativi ai progetti selezionati..."
+ label_user_mail_option_all: "Per ogni evento relativo ad uno dei miei progetti"
+ label_user_mail_option_none: "Solo per argomenti che osservo o che mi riguardano"
+ setting_emails_footer: Piè di pagina e-mail
+ label_float: Decimale
+ button_copy: Copia
+ mail_body_account_information_external: "Puoi utilizzare il tuo account {{value}} per accedere al sistema."
+ mail_body_account_information: Le informazioni riguardanti il tuo account
+ setting_protocol: Protocollo
+ label_user_mail_no_self_notified: "Non voglio notifiche riguardanti modifiche da me apportate"
+ setting_time_format: Formato ora
+ label_registration_activation_by_email: attivazione account via e-mail
+ mail_subject_account_activation_request: "{{value}} richiesta attivazione account"
+ mail_body_account_activation_request: "Un nuovo utente ({{value}}) ha effettuato la registrazione. Il suo account è in attesa di abilitazione da parte tua:"
+ label_registration_automatic_activation: attivazione account automatica
+ label_registration_manual_activation: attivazione account manuale
+ notice_account_pending: "Il tuo account è stato creato ed è in attesa di attivazione da parte dell'amministratore."
+ field_time_zone: Fuso orario
+ text_caracters_minimum: "Deve essere lungo almeno {{count}} caratteri."
+ setting_bcc_recipients: Destinatari in copia nascosta (bcc)
+ button_annotate: Annota
+ label_issues_by: "Segnalazioni di {{value}}"
+ field_searchable: Ricercabile
+ label_display_per_page: "Per pagina: {{value}}"
+ setting_per_page_options: Opzioni oggetti per pagina
+ label_age: Età
+ notice_default_data_loaded: Configurazione predefinita caricata con successo.
+ text_load_default_configuration: Carica la configurazione predefinita
+ text_no_configuration_data: "Ruoli, tracker, stati delle segnalazioni e workflow non sono stati ancora configurati.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+ error_can_t_load_default_data: "Non è stato possibile caricare la configurazione predefinita : {{value}}"
+ button_update: Aggiorna
+ label_change_properties: Modifica le proprietà
+ label_general: Generale
+ label_repository_plural: Repository
+ label_associated_revisions: Revisioni associate
+ setting_user_format: Formato visualizzazione utenti
+ text_status_changed_by_changeset: "Applicata nel changeset {{value}}."
+ label_more: Altro
+ text_issues_destroy_confirmation: 'Sei sicuro di voler eliminare le segnalazioni selezionate?'
+ label_scm: SCM
+ text_select_project_modules: 'Seleziona i moduli abilitati per questo progetto:'
+ label_issue_added: Segnalazioni aggiunte
+ label_issue_updated: Segnalazioni aggiornate
+ label_document_added: Documenti aggiunti
+ label_message_posted: Messaggi aggiunti
+ label_file_added: File aggiunti
+ label_news_added: Notizie aggiunte
+ project_module_boards: Forum
+ project_module_issue_tracking: Tracking delle segnalazioni
+ project_module_wiki: Wiki
+ project_module_files: File
+ project_module_documents: Documenti
+ project_module_repository: Repository
+ project_module_news: Notizie
+ project_module_time_tracking: Time tracking
+ text_file_repository_writable: Repository dei file scrivibile
+ text_default_administrator_account_changed: L'account amministrativo predefinito è stato modificato
+ text_rmagick_available: RMagick disponibile (opzionale)
+ button_configure: Configura
+ label_plugins: Plugin
+ label_ldap_authentication: Autenticazione LDAP
+ label_downloads_abbr: D/L
+ label_this_month: questo mese
+ label_last_n_days: "ultimi {{count}} giorni"
+ label_all_time: sempre
+ label_this_year: quest'anno
+ label_date_range: Intervallo di date
+ label_last_week: ultima settimana
+ label_yesterday: ieri
+ label_last_month: ultimo mese
+ label_add_another_file: Aggiungi un altro file
+ label_optional_description: Descrizione opzionale
+ text_destroy_time_entries_question: "{{hours}} ore risultano spese sulle segnalazioni che stai per cancellare. Cosa vuoi fare ?"
+ error_issue_not_found_in_project: 'La segnalazione non è stata trovata o non appartiene al progetto'
+ text_assign_time_entries_to_project: Assegna le ore segnalate al progetto
+ text_destroy_time_entries: Elimina le ore segnalate
+ text_reassign_time_entries: 'Riassegna le ore a questa segnalazione:'
+ setting_activity_days_default: Giorni mostrati sulle attività di progetto
+ label_chronological_order: In ordine cronologico
+ field_comments_sorting: Mostra commenti
+ label_reverse_chronological_order: In ordine cronologico inverso
+ label_preferences: Preferenze
+ setting_display_subprojects_issues: Mostra le segnalazioni dei sottoprogetti nel progetto principale per default
+ label_overall_activity: Attività generale
+ setting_default_projects_public: I nuovi progetti sono pubblici per default
+ error_scm_annotate: "L'oggetto non esiste o non può essere annotato."
+ label_planning: Pianificazione
+ text_subprojects_destroy_warning: "Anche i suoi sottoprogetti: {{value}} verranno eliminati."
+ label_and_its_subprojects: "{{value}} ed i suoi sottoprogetti"
+ mail_body_reminder: "{{count}} segnalazioni che ti sono state assegnate scadranno nei prossimi {{days}} giorni:"
+ mail_subject_reminder: "{{count}} segnalazioni in scadenza nei prossimi giorni"
+ text_user_wrote: "{{value}} ha scritto:"
+ label_duplicated_by: duplicato da
+ setting_enabled_scm: SCM abilitato
+ text_enumeration_category_reassign_to: 'Riassegnale a questo valore:'
+ text_enumeration_destroy_question: "{{count}} oggetti hanno un assegnamento su questo valore."
+ label_incoming_emails: E-mail in arrivo
+ label_generate_key: Genera una chiave
+ setting_mail_handler_api_enabled: Abilita WS per le e-mail in arrivo
+ setting_mail_handler_api_key: Chiave API
+ text_email_delivery_not_configured: "La consegna via e-mail non è configurata e le notifiche sono disabilitate.\nConfigura il tuo server SMTP in config/email.yml e riavvia l'applicazione per abilitarle."
+ field_parent_title: Parent page
+ label_issue_watchers: Osservatori
+ setting_commit_logs_encoding: Codifica dei messaggi di commit
+ button_quote: Quota
+ setting_sequential_project_identifiers: Genera progetti con identificativi in sequenza
+ notice_unable_delete_version: Impossibile cancellare la versione
+ label_renamed: rinominato
+ label_copied: copiato
+ setting_plain_text_mail: Solo testo (non HTML)
+ permission_view_files: Vedi files
+ permission_edit_issues: Modifica segnalazioni
+ permission_edit_own_time_entries: Modifica propri time logs
+ permission_manage_public_queries: Gestisci query pubbliche
+ permission_add_issues: Aggiungi segnalazioni
+ permission_log_time: Segna tempo impiegato
+ permission_view_changesets: Vedi changesets
+ permission_view_time_entries: Vedi tempi impiegati
+ permission_manage_versions: Gestisci versioni
+ permission_manage_wiki: Gestisci wiki
+ permission_manage_categories: Gestisci categorie segnalazione
+ permission_protect_wiki_pages: Proteggi pagine wiki
+ permission_comment_news: Commenta notizie
+ permission_delete_messages: Elimina messaggi
+ permission_select_project_modules: Seleziona moduli progetto
+ permission_manage_documents: Gestisci documenti
+ permission_edit_wiki_pages: Modifica pagine wiki
+ permission_add_issue_watchers: Aggiungi osservatori
+ permission_view_gantt: Vedi diagrammi gantt
+ permission_move_issues: Muovi segnalazioni
+ permission_manage_issue_relations: Gestisci relazioni tra segnalazioni
+ permission_delete_wiki_pages: Elimina pagine wiki
+ permission_manage_boards: Gestisci forum
+ permission_delete_wiki_pages_attachments: Elimina allegati
+ permission_view_wiki_edits: Vedi cronologia wiki
+ permission_add_messages: Aggiungi messaggi
+ permission_view_messages: Vedi messaggi
+ permission_manage_files: Gestisci files
+ permission_edit_issue_notes: Modifica note
+ permission_manage_news: Gestisci notizie
+ permission_view_calendar: Vedi calendario
+ permission_manage_members: Gestisci membri
+ permission_edit_messages: Modifica messaggi
+ permission_delete_issues: Elimina segnalazioni
+ permission_view_issue_watchers: Vedi lista osservatori
+ permission_manage_repository: Gestisci repository
+ permission_commit_access: Permesso di commit
+ permission_browse_repository: Sfoglia repository
+ permission_view_documents: Vedi documenti
+ permission_edit_project: Modifica progetti
+ permission_add_issue_notes: Aggiungi note
+ permission_save_queries: Salva query
+ permission_view_wiki_pages: Vedi pagine wiki
+ permission_rename_wiki_pages: Rinomina pagine wiki
+ permission_edit_time_entries: Modifica time logs
+ permission_edit_own_issue_notes: Modifica proprie note
+ setting_gravatar_enabled: Usa icone utente Gravatar
+ label_example: Esempio
+ text_repository_usernames_mapping: "Seleziona per aggiornare la corrispondenza tra gli utenti Redmine e quelli presenti nel log del repository.\nGli utenti Redmine e repository con lo stesso username o email sono mappati automaticamente."
+ permission_edit_own_messages: Modifica propri messaggi
+ permission_delete_own_messages: Elimina propri messaggi
+ label_user_activity: "attività di {{value}}"
+ label_updated_time_by: "Aggiornato da {{author}} {{age}} fa"
+ text_diff_truncated: '... Le differenze sono state troncate perchè superano il limite massimo visualizzabile.'
+ setting_diff_max_lines_displayed: Limite massimo di differenze (linee) mostrate
+ text_plugin_assets_writable: Assets directory dei plugins scrivibile
+ warning_attachments_not_saved: "{{count}} file non possono essere salvati."
+ button_create_and_continue: Crea e continua
+ text_custom_field_possible_values_info: 'Un valore per ogni linea'
+ label_display: Mostra
+ field_editable: Modificabile
+ setting_repository_log_display_limit: Numero massimo di revisioni elencate nella cronologia file
+ setting_file_max_size_displayed: Dimensione massima dei contenuti testuali visualizzati
+ field_watcher: Osservatore
+ setting_openid: Accetta login e registrazione con OpenID
+ field_identity_url: URL OpenID
+ label_login_with_open_id_option: oppure autenticati usando OpenID
+ field_content: Contenuto
+ label_descending: Discendente
+ label_sort: Ordina
+ label_ascending: Ascendente
+ label_date_from_to: Da {{start}} a {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Questa pagina ha {{descendants}} pagine figlie. Cosa ne vuoi fare?
+ text_wiki_page_reassign_children: Riassegna le pagine figlie al padre di questa pagina
+ text_wiki_page_nullify_children: Mantieni le pagine figlie come pagine radice
+ text_wiki_page_destroy_children: Elimina le pagine figlie e tutta la discendenza
+ setting_password_min_length: Minima lunghezza password
+ field_group_by: Raggruppa risultati per
+ mail_subject_wiki_content_updated: "La pagina wiki '{{page}}' è stata aggiornata"
+ label_wiki_content_added: Aggiunta pagina al wiki
+ mail_subject_wiki_content_added: "La pagina '{{page}}' è stata aggiunta al wiki"
+ mail_body_wiki_content_added: La pagina '{{page}}' è stata aggiunta al wiki da {{author}}.
+ label_wiki_content_updated: Aggiornata pagina wiki
+ mail_body_wiki_content_updated: La pagina '{{page}}' wiki è stata aggiornata da{{author}}.
+ permission_add_project: Crea progetto
+ setting_new_project_user_role_id: Ruolo assegnato agli utenti non amministratori che creano un progetto
+ 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
--- /dev/null
+# Japanese translations for Ruby on Rails
+# by Akira Matsuda (ronnie@dio.jp)
+# AR error messages are basically taken from Ruby-GetText-Package. Thanks to Masao Mutoh.
+
+ja:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y/%m/%d"
+ short: "%m/%d"
+ long: "%Y年%m月%d日(%a)"
+
+ day_names: [日曜日, 月曜日, 火曜日, 水曜日, 木曜日, 金曜日, 土曜日]
+ abbr_day_names: [日, 月, 火, 水, 木, 金, 土]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月]
+ abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月]
+ # Used in date_select and datime_select.
+ order: [:year, :month, :day]
+
+ time:
+ formats:
+ default: "%Y/%m/%d %H:%M:%S"
+ time: "%H:%M"
+ short: "%y/%m/%d %H:%M"
+ long: "%Y年%m月%d日(%a) %H時%M分%S秒 %Z"
+ am: "午前"
+ pm: "午後"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "30秒前後"
+ 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_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}} 年以上"
+
+ number:
+ format:
+ separator: "."
+ delimiter: ","
+ precision: 3
+
+ currency:
+ format:
+ format: "%n%u"
+ unit: "円"
+ separator: "."
+ delimiter: ","
+ precision: 0
+
+ percentage:
+ format:
+ delimiter: ""
+
+ precision:
+ format:
+ delimiter: ""
+
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ 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: true
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "{{model}} にエラーが発生しました。"
+ other: "{{model}} に {{count}} つのエラーが発生しました。"
+ body: "次の項目を確認してください。"
+
+ 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: "この関係では、循環依存になります"
+
+ actionview_instancetag_blank_option: 選んでください
+
+ general_text_No: 'いいえ'
+ general_text_Yes: 'はい'
+ general_text_no: 'いいえ'
+ general_text_yes: 'はい'
+ general_lang_name: 'Japanese (日本語)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: CP932
+ general_pdf_encoding: CP932
+ general_first_day_of_week: '7'
+
+ notice_account_updated: アカウントが更新されました。
+ notice_account_invalid_creditentials: ユーザ名もしくはパスワードが無効
+ notice_account_password_updated: パスワードが更新されました。
+ notice_account_wrong_password: パスワードが違います
+ notice_account_register_done: アカウントが作成されました。
+ notice_account_unknown_email: ユーザが存在しません。
+ notice_can_t_change_password: このアカウントでは外部認証を使っています。パスワードは変更できません。
+ notice_account_lost_email_sent: 新しいパスワードのメールを送信しました。
+ notice_account_activated: アカウントが有効になりました。ログインできます。
+ notice_successful_create: 作成しました。
+ notice_successful_update: 更新しました。
+ notice_successful_delete: 削除しました。
+ notice_successful_connection: 接続しました。
+ notice_file_not_found: アクセスしようとしたページは存在しないか削除されています。
+ notice_locking_conflict: 別のユーザがデータを更新しています。
+ notice_not_authorized: このページにアクセスするには認証が必要です。
+ notice_email_sent: "{{value}} 宛にメールを送信しました。"
+ notice_email_error: "メール送信中にエラーが発生しました ({{value}})"
+ notice_feeds_access_key_reseted: RSSアクセスキーを初期化しました。
+ notice_failed_to_save_issues: "{{total}} 件のうち {{count}} 件のチケットが保存できませんでした: {{ids}}."
+ notice_no_issue_selected: "チケットが選択されていません! 更新対象のチケットを選択してください。"
+ notice_account_pending: アカウントは作成済みで、管理者の承認待ちです。
+ notice_default_data_loaded: デフォルト設定をロードしました。
+ notice_unable_delete_version: バージョンを削除できません
+
+ error_can_t_load_default_data: "デフォルト設定がロードできませんでした: {{value}}"
+ error_scm_not_found: リポジトリに、エントリ/リビジョンが存在しません。
+ error_scm_command_failed: "リポジトリへアクセスしようとしてエラーになりました: {{value}}"
+ error_scm_annotate: "エントリが存在しない、もしくはアノテートできません。"
+ error_issue_not_found_in_project: 'チケットが見つかりません、もしくはこのプロジェクトに属していません'
+ error_no_tracker_in_project: 'このプロジェクトにはトラッカーが登録されていません。プロジェクト設定を確認してください。'
+ error_no_default_issue_status: 'デフォルトのチケットステータスが定義されていません。設定を確認してください(管理→チケットのステータス)。'
+
+ 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}} 件のチケットが期限間近です"
+ mail_body_reminder: "{{count}} 件の担当チケットの期限が {{days}} 日以内に到来します:"
+ mail_subject_wiki_content_added: "Wikiページ {{page}} が追加されました"
+ mail_body_wiki_content_added: "{{author}} によってWikiページ {{page}} が追加されました。"
+ mail_subject_wiki_content_updated: "Wikiページ {{page}} が更新されました"
+ mail_body_wiki_content_updated: "{{author}} によってWikiページ {{page}} が更新されました。"
+
+ gui_validation_error: 1 件のエラー
+ gui_validation_error_plural: "{{count}} 件のエラー"
+
+ field_name: 名前
+ field_description: 説明
+ field_summary: サマリ
+ field_is_required: 必須
+ field_firstname: 名前
+ field_lastname: 苗字
+ field_mail: メールアドレス
+ field_filename: ファイル
+ field_filesize: サイズ
+ field_downloads: ダウンロード
+ field_author: 作成者
+ field_created_on: 作成日
+ field_updated_on: 更新日
+ field_field_format: 書式
+ field_is_for_all: 全プロジェクト向け
+ field_possible_values: 選択肢
+ field_regexp: 正規表現
+ field_min_length: 最小値
+ field_max_length: 最大値
+ field_value: 値
+ field_category: カテゴリ
+ field_title: タイトル
+ field_project: プロジェクト
+ field_issue: チケット
+ field_status: ステータス
+ field_notes: 注記
+ field_is_closed: 終了したチケット
+ field_is_default: デフォルト値
+ field_tracker: トラッカー
+ field_subject: 題名
+ field_due_date: 期限日
+ field_assigned_to: 担当者
+ field_priority: 優先度
+ field_fixed_version: 対象バージョン
+ field_user: ユーザ
+ field_role: ロール
+ field_homepage: ホームページ
+ field_is_public: 公開
+ field_parent: 親プロジェクト名
+ field_is_in_chlog: 変更履歴に表示されているチケット
+ 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: 検索範囲
+ 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: グループ条件
+
+ 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: リポジトリ管理用の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_repositories_encodings: リポジトリのエンコーディング
+ setting_commit_logs_encoding: コミットメッセージのエンコーディング
+ setting_emails_footer: メールのフッタ
+ setting_protocol: プロトコル
+ setting_per_page_options: ページ毎の表示件数
+ setting_user_format: ユーザ名の表示書式
+ setting_activity_days_default: プロジェクトの活動ページに表示される日数
+ setting_display_subprojects_issues: デフォルトでサブプロジェクトのチケットをメインプロジェクトに表示する
+ setting_enabled_scm: 使用するバージョン管理システム
+ setting_mail_handler_api_enabled: 受信メール用のWeb Serviceを有効にする
+ setting_mail_handler_api_key: APIキー
+ setting_sequential_project_identifiers: プロジェクト識別子を連番で生成する
+ setting_gravatar_enabled: 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: 管理者以外のユーザが作成したプロジェクトに設定するロール
+
+ permission_add_project: プロジェクトの追加
+ permission_edit_project: プロジェクトの編集
+ permission_select_project_modules: モジュールの選択
+ permission_manage_members: メンバーの管理
+ permission_manage_versions: バージョンの管理
+ permission_manage_categories: チケットのカテゴリの管理
+ permission_add_issues: チケットの追加
+ permission_edit_issues: チケットの編集
+ permission_manage_issue_relations: チケットの管理
+ permission_add_issue_notes: 注記の追加
+ permission_edit_issue_notes: 注記の編集
+ permission_edit_own_issue_notes: 自身が記入した注記の編集
+ permission_move_issues: チケットの移動
+ permission_delete_issues: チケットの削除
+ permission_manage_public_queries: 公開クエリの管理
+ permission_save_queries: クエリの保存
+ permission_view_gantt: ガントチャートの閲覧
+ permission_view_calendar: カレンダーの閲覧
+ permission_view_issue_watchers: 監視者一覧の閲覧
+ permission_add_issue_watchers: 監視者の追加
+ permission_log_time: 変更履歴の記入
+ permission_view_time_entries: 変更履歴の閲覧
+ permission_edit_time_entries: 変更履歴の編集
+ permission_edit_own_time_entries: 自身が記入した変更履歴の編集
+ permission_manage_news: ニュースの管理
+ permission_comment_news: ニュースへのコメント
+ permission_manage_documents: 文書の管理
+ permission_view_documents: 文書の閲覧
+ permission_manage_files: ファイルの管理
+ permission_view_files: ファイルの閲覧
+ permission_manage_wiki: 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: 自身が記入したメッセージの削除
+
+ project_module_issue_tracking: チケットトラッキング
+ project_module_time_tracking: 時間トラッキング
+ project_module_news: ニュース
+ project_module_documents: 文書
+ project_module_files: ファイル
+ project_module_wiki: Wiki
+ project_module_repository: リポジトリ
+ project_module_boards: フォーラム
+
+ label_user: ユーザ
+ label_user_plural: ユーザ
+ label_user_new: 新しいユーザ
+ label_project: プロジェクト
+ label_project_new: 新しいプロジェクト
+ label_project_plural: プロジェクト
+ label_x_projects:
+ zero: プロジェクトはありません
+ 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_administration: 管理
+ label_login: ログイン
+ label_logout: ログアウト
+ label_help: ヘルプ
+ label_reported_issues: 報告したチケット
+ label_assigned_to_me_issues: 担当しているチケット
+ label_last_login: 最近の接続
+ label_registered_on: 登録日
+ label_activity: 活動
+ label_overall_activity: 全ての活動
+ label_user_activity: "{{value}} の活動"
+ label_new: 新しく作成
+ label_logged_as: ログイン中:
+ label_environment: 環境
+ label_authentication: 認証
+ label_auth_source: 認証モード
+ label_auth_source_new: 新しい認証モード
+ label_auth_source_plural: 認証モード
+ label_subproject_plural: サブプロジェクト
+ label_and_its_subprojects: "{{value}} とサブプロジェクト"
+ label_min_max_length: 最小値 - 最大値の長さ
+ label_list: リストから選択
+ label_date: 日付
+ label_integer: 整数
+ label_float: 小数
+ label_boolean: 真偽値
+ label_string: テキスト
+ label_text: 長いテキスト
+ label_attribute: 属性
+ label_attribute_plural: 属性
+ label_download: "{{count}} ダウンロード"
+ label_download_plural: "{{count}} ダウンロード"
+ label_no_data: 表示するデータがありません
+ label_change_status: ステータスの変更
+ label_history: 履歴
+ label_attachment: 添付ファイル
+ label_attachment_new: 新しい添付ファイル
+ label_attachment_delete: 添付ファイルを削除
+ label_attachment_plural: 添付ファイル
+ label_file_added: ファイルが追加されました
+ label_report: レポート
+ label_report_plural: レポート
+ label_news: ニュース
+ label_news_new: ニュースを追加
+ label_news_plural: ニュース
+ label_news_latest: 最新ニュース
+ label_news_view_all: 全てのニュースを見る
+ label_news_added: ニュースが追加されました
+ label_change_log: 変更履歴
+ label_settings: 設定
+ label_overview: 概要
+ label_version: バージョン
+ label_version_new: 新しいバージョン
+ label_version_plural: バージョン
+ label_confirmation: 確認
+ label_export_to: 他の形式に出力
+ label_read: 読む...
+ label_public_projects: 公開プロジェクト
+ label_open_issues: 進行中
+ label_open_issues_plural: 進行中
+ label_closed_issues: 終了
+ label_closed_issues_plural: 終了
+ label_x_open_issues_abbr_on_total:
+ zero: 0 件進行中 / 全 {{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_modification: "{{count}} 点の変更"
+ label_modification_plural: "{{count}} 点の変更"
+ label_branch: ブランチ
+ label_tag: タグ
+ label_revision: リビジョン
+ label_revision_plural: リビジョン
+ label_associated_revisions: 関係しているリビジョン
+ label_added: 追加
+ label_modified: 変更
+ label_copied: コピー
+ label_renamed: 名称変更
+ label_deleted: 削除
+ label_latest_revision: 最新リビジョン
+ label_latest_revision_plural: 最新リビジョン
+ label_view_revisions: リビジョンを見る
+ label_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_f_hour: "{{value}} 時間"
+ label_f_hour_plural: "{{value}} 時間"
+ label_time_tracking: 時間トラッキング
+ label_change_plural: 変更
+ label_statistics: 統計
+ label_commits_per_month: 月別のコミット
+ label_commits_per_author: 起票者別のコミット
+ label_view_diff: 差分を見る
+ label_diff_inline: インライン
+ label_diff_side_by_side: 横に並べる
+ label_options: オプション
+ label_copy_workflow_from: ワークフローをここからコピー
+ label_permissions_report: 権限レポート
+ label_watched_issues: 監視中のチケット
+ label_related_issues: 関連するチケット
+ label_applied_status: 適用されたステータス
+ label_loading: ロード中...
+ label_relation_new: 新しい関連
+ label_relation_delete: 関連の削除
+ label_relates_to: 関係している
+ label_duplicates: 重複している
+ label_duplicated_by: 重複している
+ label_blocks: ブロックしている
+ label_blocked_by: ブロックされている
+ label_precedes: 先行する
+ label_follows: 後続する
+ label_end_to_start: 最後-最初
+ label_end_to_end: 最後-最後
+ label_start_to_start: 最初-最初
+ label_start_to_end: 最初-最後
+ label_stay_logged_in: ログインを維持
+ label_disabled: 無効
+ label_show_completed_versions: 完了したバージョンを表示
+ label_me: 自分
+ label_board: フォーラム
+ label_board_new: 新しいフォーラム
+ label_board_plural: フォーラム
+ label_topic_plural: トピック
+ label_message_plural: メッセージ
+ label_message_last: 最新のメッセージ
+ label_message_new: 新しいメッセージ
+ label_message_posted: メッセージが追加されました
+ label_reply_plural: 返答
+ label_send_information: アカウント情報をユーザに送信
+ label_year: 年
+ label_month: 月
+ label_week: 週
+ label_date_from: "日付指定: "
+ label_date_to: から
+ label_language_based: 既定の言語の設定に従う
+ label_sort_by: "{{value}} で並び替え"
+ label_send_test_email: テストメールを送信
+ label_feeds_access_key_created_on: "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_selected: "選択したプロジェクト..."
+ label_user_mail_option_all: "参加しているプロジェクトの全てのチケット"
+ label_user_mail_option_none: "監視または関係しているチケットのみ"
+ label_user_mail_no_self_notified: 自分自身による変更の通知は不要です
+ label_registration_activation_by_email: メールでアカウントを有効化
+ label_registration_automatic_activation: 自動でアカウントを有効化
+ label_registration_manual_activation: 手動でアカウントを有効化
+ label_display_per_page: "1ページに: {{value}}"
+ label_age: 年齢
+ label_change_properties: プロパティの変更
+ label_general: 全般
+ label_more: 続き
+ label_scm: バージョン管理システム
+ label_plugins: プラグイン
+ label_ldap_authentication: LDAP認証
+ label_downloads_abbr: DL
+ 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: 活動時間の記録
+
+ button_login: ログイン
+ button_submit: 変更
+ button_save: 保存
+ button_check_all: チェックを全部つける
+ button_uncheck_all: チェックを全部外す
+ button_delete: 削除
+ button_create: 作成
+ button_create_and_continue: 連続作成
+ button_test: テスト
+ button_edit: 編集
+ button_add: 追加
+ button_change: 変更
+ button_apply: 適用
+ button_clear: クリア
+ button_lock: ロック
+ button_unlock: アンロック
+ button_download: ダウンロード
+ button_list: 一覧
+ button_view: 見る
+ button_move: 移動
+ button_back: 戻る
+ button_cancel: キャンセル
+ button_activate: 有効にする
+ button_sort: ソート
+ button_log_time: 時間を記録
+ button_rollback: このバージョンにロールバック
+ button_watch: 監視
+ button_unwatch: 監視をやめる
+ button_reply: 返答
+ button_archive: 書庫に保存
+ button_unarchive: 書庫から戻す
+ button_reset: リセット
+ button_rename: 名前変更
+ button_change_password: パスワード変更
+ button_copy: コピー
+ button_annotate: 注釈
+ button_update: 更新
+ button_configure: 設定
+ button_quote: 引用
+
+ status_active: 有効
+ status_registered: 登録
+ status_locked: ロック
+
+ text_select_mail_notifications: どのメール通知を送信するか、アクションを選択してください。
+ text_regexp_info: 例) ^[A-Z0-9]+$
+ text_min_max_length_info: 0だと無制限になります
+ text_project_destroy_confirmation: 本当にこのプロジェクトと関連データを削除してもよろしいですか?
+ text_subprojects_destroy_warning: "サブプロジェクト {{value}} も削除されます。"
+ text_workflow_edit: ワークフローを編集するロールとトラッカーを選んでください
+ text_are_you_sure: よろしいですか?
+ text_journal_changed: "{{label}} を {{old}} から {{new}} に変更"
+ text_journal_set_to: "{{label}} を {{value}} にセット"
+ text_journal_deleted: "{{label}} を削除 ({{old}})"
+ text_tip_task_begin_day: この日に開始するタスク
+ text_tip_task_end_day: この日に終了するタスク
+ text_tip_task_begin_end_day: この日のうちに開始して終了するタスク
+ text_project_identifier_info: '英小文字(a-z)と数字とダッシュ(-)が使えます。<br />一度保存すると、識別子は変更できません。'
+ text_caracters_maximum: "最大 {{count}} 文字です。"
+ text_caracters_minimum: "最低 {{count}} 文字の長さが必要です"
+ text_length_between: "長さは {{min}} から {{max}} 文字までです。"
+ text_tracker_no_workflow: このトラッカーにワークフローが定義されていません
+ text_unallowed_characters: 次の文字は使用できません
+ text_comma_separated: (カンマで区切った)複数の値が使えます
+ text_issues_ref_in_commit_messages: コミットメッセージ内でチケットの参照/修正
+ text_issue_added: "チケット {{id}} が {{author}} によって報告されました。"
+ text_issue_updated: "チケット {{id}} が {{author}} によって更新されました。"
+ text_wiki_destroy_confirmation: 本当にこのwikiとその内容の全てを削除しますか?
+ text_issue_category_destroy_question: "{{count}} 件のチケットがこのカテゴリに割り当てられています。"
+ text_issue_category_destroy_assignments: カテゴリの割り当てを削除する
+ text_issue_category_reassign_to: チケットをこのカテゴリに再割り当てする
+ text_user_mail_option: "未選択のプロジェクトでは、監視または関係しているチケット(例: 自分が報告者もしくは担当者であるチケット)のみメールが送信されます。"
+ text_no_configuration_data: "ロール、トラッカー、チケットのステータス、ワークフローがまだ設定されていません。\nデフォルト設定のロードを強くお勧めします。ロードした後、それを修正することができます。"
+ text_load_default_configuration: デフォルト設定をロード
+ text_status_changed_by_changeset: "更新履歴 {{value}} で適用されました。"
+ text_issues_destroy_confirmation: '本当に選択したチケットを削除しますか?'
+ text_select_project_modules: 'このプロジェクトで使用するモジュールを選択してください:'
+ text_default_administrator_account_changed: デフォルト管理アカウントが変更済
+ text_file_repository_writable: ファイルリポジトリに書き込み可能
+ text_plugin_assets_writable: Plugin assetsディレクトリに書き込み可能
+ 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: "メールを送信するために必要な設定が行われていないため、メール通知は利用できません。\nconfig/email.ymlでSMTPサーバの設定を行い、アプリケーションを再起動してください。"
+ text_repository_usernames_mapping: "リポジトリのログから検出されたユーザー名をどのRedmineユーザーに関連づけるのか選択してください。\nログ上のユーザー名またはメールアドレスがRedmineのユーザーと一致する場合は自動的に関連づけられます。"
+ text_diff_truncated: '... 差分の行数が表示可能な上限を超えました。超過分は表示しません。'
+ text_custom_field_possible_values_info: '選択肢の値は1行に1個ずつ記述してください。'
+ text_wiki_page_destroy_question: "この親ページの配下に {{descendants}} つの子孫ページがあります。"
+ text_wiki_page_nullify_children: "子ページをメインページ配下に移動する"
+ text_wiki_page_destroy_children: "配下の子孫ページも削除する"
+ text_wiki_page_reassign_children: "子ページを次の親ページの配下に移動する"
+
+ default_role_manager: 管理者
+ default_role_developper: 開発者
+ default_role_reporter: 報告者
+ default_tracker_bug: バグ
+ default_tracker_feature: 機能
+ default_tracker_support: サポート
+ default_issue_status_new: 新規
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: 解決
+ default_issue_status_feedback: フィードバック
+ default_issue_status_closed: 終了
+ default_issue_status_rejected: 却下
+ default_doc_category_user: ユーザ文書
+ default_doc_category_tech: 技術文書
+ default_priority_low: 低め
+ default_priority_normal: 通常
+ default_priority_high: 高め
+ default_priority_urgent: 急いで
+ default_priority_immediate: 今すぐ
+ default_activity_design: 設計作業
+ default_activity_development: 開発作業
+
+ enumeration_issue_priorities: チケットの優先度
+ enumeration_doc_categories: 文書カテゴリ
+ enumeration_activities: 作業分類 (時間トラッキング)
+ 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
--- /dev/null
+# Korean translations for Ruby on Rails
+# by Kihyun Yoon(ddumbugie@gmail.com),http://plenum.textcube.com/
+# by John Hwang (jhwang@tavon.org),http://github.com/tavon
+# by Yonghwan SO(please insert your email), last update at 2009-09-11
+# last update at 2009-09-11 by Kihyun Yoon
+ko:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y/%m/%d"
+ short: "%m/%d"
+ long: "%Y년 %m월 %d일 (%a)"
+
+ day_names: [일요일, 월요일, 화요일, 수요일, 목요일, 금요일, 토요일]
+ abbr_day_names: [일, 월, 화, 수, 목, 금, 토]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, 1월, 2월, 3월, 4월, 5월, 6월, 7월, 8월, 9월, 10월, 11월, 12월]
+ abbr_month_names: [~, 1월, 2월, 3월, 4월, 5월, 6월, 7월, 8월, 9월, 10월, 11월, 12월]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%Y/%m/%d %H:%M:%S"
+ time: "%H:%M"
+ short: "%y/%m/%d %H:%M"
+ long: "%Y년 %B월 %d일, %H시 %M분 %S초 %Z"
+ am: "오전"
+ pm: "오후"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "30초"
+ less_than_x_seconds:
+ one: "일초 이하"
+ other: "{{count}}초 이하"
+ x_seconds:
+ one: "일초"
+ other: "{{count}}초"
+ less_than_x_minutes:
+ one: "일분 이하"
+ other: "{{count}}분 이하"
+ x_minutes:
+ one: "일분"
+ other: "{{count}}분"
+ about_x_hours:
+ one: "약 한시간"
+ other: "약 {{count}}시간"
+ x_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}}년 이상"
+ prompts:
+ year: "년"
+ month: "월"
+ day: "일"
+ hour: "시"
+ minute: "분"
+ second: "초"
+
+ number:
+ # Used in number_with_delimiter()
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format:
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: "."
+ # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: ","
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 3
+
+ # Used in number_to_currency()
+ currency:
+ format:
+ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
+ format: "%u%n"
+ unit: "₩"
+ # These three are to override number.format and are optional
+ separator: "."
+ delimiter: ","
+ precision: 0
+
+ # Used in number_to_percentage()
+ percentage:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_precision()
+ precision:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_human_size()
+ human:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+# Used in array.to_sentence.
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: "과 "
+ last_word_connector: ", "
+ sentence_connector: "그리고"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "한개의 오류가 발생해 {{model}}을(를) 저장하지 않았습니다."
+ other: "{{count}}개의 오류가 발생해 {{model}}을(를) 저장하지 않았습니다."
+ # The variable :count is also available
+ body: "다음 항목에 문제가 발견했습니다:"
+
+ messages:
+ inclusion: "은 목록에 포함되어 있지 않습니다"
+ exclusion: "은 예약되어 있습니다"
+ invalid: "은 유효하지 않습니다."
+ confirmation: "은 확인이 되지 않았습니다"
+ accepted: "은 인정되어야 합니다"
+ empty: "은 길이가 0이어서는 안됩니다."
+ blank: "은 빈 값이어서는 안 됩니다"
+ too_long: "은 너무 깁니다 (최대 {{count}}자 까지)"
+ too_short: "은 너무 짧습니다 (최소 {{count}}자 까지)"
+ wrong_length: "은 길이가 틀렸습니다 ({{count}}자이어야 합니다.)"
+ taken: "은 이미 선택된 겁니다"
+ not_a_number: "은 숫자가 아닙니다"
+ greater_than: "은 {{count}}보다 커야 합니다."
+ greater_than_or_equal_to: "은 {{count}}보다 크거나 같아야 합니다"
+ equal_to: "은 {{count}}(와)과 같아야 합니다"
+ less_than: "은 {{count}}보다 작어야 합니다"
+ less_than_or_equal_to: "은 {{count}}과 같거나 이하을 요구합니다"
+ odd: "은 홀수여야 합니다"
+ even: "은 짝수여야 합니다"
+ greater_than_start_date: "는 시작날짜보다 커야 합니다"
+ not_same_project: "는 같은 프로젝트에 속해 있지 않습니다"
+ circular_dependency: "이 관계는 순환 의존관계를 만들 수 있습니다"
+
+ actionview_instancetag_blank_option: 선택하세요
+
+ general_text_No: '아니오'
+ general_text_Yes: '예'
+ general_text_no: '아니오'
+ general_text_yes: '예'
+ general_lang_name: '한국어(Korean)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '7'
+
+ notice_account_updated: 계정이 성공적으로 변경되었습니다.
+ notice_account_invalid_creditentials: 잘못된 계정 또는 비밀번호
+ notice_account_password_updated: 비밀번호가 잘 변경되었습니다.
+ notice_account_wrong_password: 잘못된 비밀번호
+ notice_account_register_done: 계정이 잘 만들어졌습니다. 계정을 활성화하시려면 받은 메일의 링크를 클릭해주세요.
+ notice_account_unknown_email: 알려지지 않은 사용자.
+ notice_can_t_change_password: 이 계정은 외부 인증을 이용합니다. 비밀번호를 변경할 수 없습니다.
+ notice_account_lost_email_sent: 새로운 비밀번호를 위한 메일이 발송되었습니다.
+ notice_account_activated: 계정이 활성화되었습니다. 이제 로그인 하실수 있습니다.
+ notice_successful_create: 생성 성공.
+ notice_successful_update: 변경 성공.
+ notice_successful_delete: 삭제 성공.
+ notice_successful_connection: 연결 성공.
+ notice_file_not_found: 요청하신 페이지는 삭제되었거나 옮겨졌습니다.
+ notice_locking_conflict: 다른 사용자에 의해서 데이터가 변경되었습니다.
+ notice_not_authorized: 이 페이지에 접근할 권한이 없습니다.
+ notice_email_sent: "{{value}}님에게 메일이 발송되었습니다."
+ notice_email_error: "메일을 전송하는 과정에 오류가 발생했습니다. ({{value}})"
+ notice_feeds_access_key_reseted: RSS에 접근가능한 열쇠(key)가 생성되었습니다.
+ notice_failed_to_save_issues: "저장에 실패하였습니다: 실패 {{count}}(선택 {{total}}): {{ids}}."
+ notice_no_issue_selected: "일감이 선택되지 않았습니다. 수정하기 원하는 일감을 선택하세요"
+ notice_account_pending: "계정이 만들어졌으며 관리자 승인 대기중입니다."
+ notice_default_data_loaded: 기본값을 성공적으로 읽어들였습니다.
+ notice_unable_delete_version: 삭제할 수 없는 버전입니다.
+
+ error_can_t_load_default_data: "기본값을 읽어들일 수 없습니다.: {{value}}"
+ error_scm_not_found: 항목이나 리비젼이 저장소에 존재하지 않습니다.
+ error_scm_command_failed: "저장소에 접근하는 도중에 오류가 발생하였습니다.: {{value}}"
+ error_scm_annotate: "항목이 없거나 행별 이력을 볼 수 없습니다."
+ error_issue_not_found_in_project: '일감이 없거나 이 프로젝트의 것이 아닙니다.'
+
+ warning_attachments_not_saved: "{{count}}개 파일을 저장할 수 없습니다."
+
+ mail_subject_lost_password: "{{value}} 비밀번호"
+ mail_body_lost_password: '비밀번호를 변경하려면 다음 링크를 클릭하세요.'
+ mail_subject_register: "{{value}} 계정 활성화"
+ mail_body_register: '계정을 활성화하려면 링크를 클릭하세요.:'
+ mail_body_account_information_external: "로그인할 때 {{value}} 계정을 사용하실 수 있습니다."
+ mail_body_account_information: 계정 정보
+ mail_subject_account_activation_request: "{{value}} 계정 활성화 요청"
+ mail_body_account_activation_request: "새 사용자({{value}})가 등록되었습니다. 관리자님의 승인을 기다리고 있습니다.:"
+ mail_body_reminder: "당신이 맡고 있는 일감 {{count}}개의 완료 기한이 {{days}}일 후 입니다."
+ mail_subject_reminder: "내일이 만기인 일감 {{count}}개"
+ mail_subject_wiki_content_added: "위키페이지 '{{page}}'이(가) 추가되었습니다."
+ mail_subject_wiki_content_updated: "'위키페이지 {{page}}'이(가) 수정되었습니다."
+ mail_body_wiki_content_added: "{{author}}이(가) 위키페이지 '{{page}}'을(를) 추가하였습니다."
+ mail_body_wiki_content_updated: "{{author}}이(가) 위키페이지 '{{page}}'을(를) 수정하였습니다."
+
+ gui_validation_error: 에러
+ gui_validation_error_plural: "{{count}}개 에러"
+
+ field_name: 이름
+ field_description: 설명
+ field_summary: 요약
+ field_is_required: 필수
+ field_firstname: 이름
+ field_lastname: 성
+ field_mail: 메일
+ field_filename: 파일
+ field_filesize: 크기
+ field_downloads: 다운로드
+ field_author: 저자
+ field_created_on: 등록
+ field_updated_on: 변경
+ field_field_format: 형식
+ field_is_for_all: 모든 프로젝트
+ field_possible_values: 가능한 값들
+ field_regexp: 정규식
+ field_min_length: 최소 길이
+ field_max_length: 최대 길이
+ field_value: 값
+ field_category: 범주
+ field_title: 제목
+ field_project: 프로젝트
+ field_issue: 일감
+ field_status: 상태
+ field_notes: 덧글
+ field_is_closed: 완료 상태
+ field_is_default: 기본값
+ field_tracker: 유형
+ field_subject: 제목
+ field_due_date: 완료 기한
+ field_assigned_to: 담당자
+ field_priority: 우선순위
+ field_fixed_version: 목표버전
+ field_user: 사용자
+ field_role: 역할
+ field_homepage: 홈페이지
+ field_is_public: 공개
+ field_parent: 상위 프로젝트
+ field_is_in_chlog: 일감 변경이력에 표시
+ field_is_in_roadmap: 로드맵에 표시
+ field_login: 로그인
+ field_mail_notification: 메일 알림
+ field_admin: 관리자
+ field_last_login_on: 마지막 로그인
+ field_language: 언어
+ field_effective_date: 일자
+ field_password: 비밀번호
+ field_new_password: 새 비밀번호
+ field_password_confirmation: 비밀번호 확인
+ field_version: 버전
+ field_type: 방식
+ field_host: 호스트
+ field_port: 포트
+ field_account: 계정
+ field_base_dn: 기본 DN
+ field_attr_login: 로그인 속성
+ field_attr_firstname: 이름 속성
+ field_attr_lastname: 성 속성
+ field_attr_mail: 메일 속성
+ field_onthefly: 동적 사용자 생성
+ field_start_date: 시작시간
+ field_done_ratio: 진척도
+ field_auth_source: 인증 공급자
+ field_hide_mail: 메일 주소 숨기기
+ field_comments: 설명
+ field_url: URL
+ field_start_page: 첫 페이지
+ field_subproject: 하위 프로젝트
+ field_hours: 시간
+ field_activity: 작업종류
+ field_spent_on: 작업시간
+ field_identifier: 식별자
+ field_is_filter: 검색조건으로 사용됨
+ field_issue_to_id: 연관된 일감
+ field_delay: 지연
+ field_assignable: 이 역할에게 일감을 맡길 수 있음
+ field_redirect_existing_links: 기존의 링크로 돌려보냄(redirect)
+ field_estimated_hours: 추정시간
+ field_column_names: 컬럼
+ field_default_value: 기본값
+ field_time_zone: 시간대
+ field_searchable: 검색가능
+ field_comments_sorting: 댓글 정렬
+ field_parent_title: 상위 제목
+ field_editable: 편집가능
+ field_watcher: 일감지킴이
+ field_identity_url: OpenID URL
+ field_content: 내용
+ field_group_by: 결과를 묶어 보여줄 기준
+
+ setting_app_title: 레드마인 제목
+ setting_app_subtitle: 레드마인 부제목
+ setting_welcome_text: 환영 메시지
+ setting_default_language: 기본 언어
+ setting_login_required: 인증이 필요함
+ setting_self_registration: 사용자 직접등록
+ setting_attachment_max_size: 최대 첨부파일 크기
+ setting_issues_export_limit: 일감 내보내기 제한
+ setting_mail_from: 발신 메일 주소
+ setting_bcc_recipients: 참조자들을 bcc로 숨기기
+ setting_plain_text_mail: 텍스트만 (HTML 없이)
+ setting_host_name: 호스트 이름과 경로
+ setting_text_formatting: 편집 방식
+ setting_wiki_compression: 위키 이력 압축
+ setting_feeds_limit: 피드에 포함할 항목의 수
+ setting_default_projects_public: 새 프로젝트를 공개로 설정
+ setting_autofetch_changesets: 제출(commit)된 변경묶음을 자동으로 가져오기
+ setting_sys_api_enabled: 저장소 관리에 WS를 사용
+ setting_commit_ref_keywords: 일감 참조에 사용할 키워드들
+ setting_commit_fix_keywords: 일감 해결에 사용할 키워드들
+ setting_autologin: 자동 로그인
+ setting_date_format: 날짜 형식
+ setting_time_format: 시간 형식
+ setting_cross_project_issue_relations: 다른 프로젝트의 일감과 연결하는 것을 허용
+ setting_issue_list_default_columns: 일감 목록에 표시할 항목
+ setting_repositories_encodings: 저장소 인코딩
+ setting_commit_logs_encoding: 제출(commit) 기록 인코딩
+ 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_api_enabled: 수신 메일에 WS를 허용
+ setting_mail_handler_api_key: API 키
+ setting_sequential_project_identifiers: 프로젝트 식별자를 순차적으로 생성
+ setting_gravatar_enabled: 그라바타 사용자 아이콘 사용
+ setting_diff_max_lines_displayed: 차이점(diff) 보기에 표시할 최대 줄수
+ setting_repository_log_display_limit: 저장소 보기에 표시할 개정판 이력의 최대 갯수
+ setting_file_max_size_displayed: 바로 보여줄 텍스트파일의 최대 크기
+ setting_openid: OpenID 로그인과 등록 허용
+ setting_password_min_length: 최소 암호 길이
+ setting_new_project_user_role_id: 프로젝트를 만든 사용자에게 주어질 역할
+
+ permission_add_project: 프로젝트 생성
+ permission_edit_project: 프로젝트 편집
+ permission_select_project_modules: 프로젝트 모듈 선택
+ permission_manage_members: 구성원 관리
+ permission_manage_versions: 버전 관리
+ permission_manage_categories: 일감 범주 관리
+ permission_add_issues: 일감 추가
+ permission_edit_issues: 일감 편집
+ permission_manage_issue_relations: 일감 관계 관리
+ permission_add_issue_notes: 덧글 추가
+ permission_edit_issue_notes: 덧글 편집
+ permission_edit_own_issue_notes: 내 덧글 편집
+ permission_move_issues: 일감 이동
+ permission_delete_issues: 일감 삭제
+ permission_manage_public_queries: 공용 검색양식 관리
+ permission_save_queries: 검색양식 저장
+ permission_view_gantt: Gantt차트 보기
+ permission_view_calendar: 달력 보기
+ permission_view_issue_watchers: 일감지킴이 보기
+ permission_add_issue_watchers: 일감지킴이 추가
+ permission_log_time: 작업시간 기록
+ permission_view_time_entries: 시간입력 보기
+ permission_edit_time_entries: 시간입력 편집
+ permission_edit_own_time_entries: 내 시간입력 편집
+ permission_manage_news: 뉴스 관리
+ permission_comment_news: 뉴스에 댓글달기
+ permission_manage_documents: 문서 관리
+ permission_view_documents: 문서 보기
+ permission_manage_files: 파일관리
+ permission_view_files: 파일보기
+ permission_manage_wiki: 위키 관리
+ permission_rename_wiki_pages: 위키 페이지 이름변경
+ permission_delete_wiki_pages: 위치 페이지 삭제
+ permission_view_wiki_pages: 위키 보기
+ permission_view_wiki_edits: 위키 기록 보기
+ permission_edit_wiki_pages: 위키 페이지 편집
+ permission_delete_wiki_pages_attachments: 첨부파일 삭제
+ permission_protect_wiki_pages: 프로젝트 위키 페이지
+ permission_manage_repository: 저장소 관리
+ permission_browse_repository: 저장소 둘러보기
+ permission_view_changesets: 변경묶음보기
+ permission_commit_access: 변경로그 보기
+ permission_manage_boards: 게시판 관리
+ permission_view_messages: 메시지 보기
+ permission_add_messages: 메시지 추가
+ permission_edit_messages: 메시지 편집
+ permission_edit_own_messages: 자기 메시지 편집
+ permission_delete_messages: 메시지 삭제
+ permission_delete_own_messages: 자기 메시지 삭제
+
+ project_module_issue_tracking: 일감관리
+ project_module_time_tracking: 시간추적
+ project_module_news: 뉴스
+ project_module_documents: 문서
+ project_module_files: 파일
+ project_module_wiki: 위키
+ project_module_repository: 저장소
+ project_module_boards: 게시판
+
+ label_user: 사용자
+ label_user_plural: 사용자
+ label_user_new: 새 사용자
+ label_project: 프로젝트
+ label_project_new: 새 프로젝트
+ label_project_plural: 프로젝트
+ label_x_projects:
+ zero: 없음
+ one: "한 프로젝트"
+ other: "{{count}}개 프로젝트"
+ label_project_all: 모든 프로젝트
+ label_project_latest: 최근 프로젝트
+ label_issue: 일감
+ label_issue_new: 새 일감만들기
+ label_issue_plural: 일감
+ label_issue_view_all: 모든 일감 보기
+ label_issues_by: "{{value}}별 일감"
+ label_issue_added: 일감 추가
+ label_issue_updated: 일감 수정
+ label_document: 문서
+ label_document_new: 새 문서
+ label_document_plural: 문서
+ label_document_added: 문서 추가
+ label_role: 역할
+ label_role_plural: 역할
+ label_role_new: 새 역할
+ label_role_and_permissions: 역할 및 권한
+ label_member: 담당자
+ label_member_new: 새 담당자
+ label_member_plural: 담당자
+ label_tracker: 일감 유형
+ label_tracker_plural: 일감 유형
+ label_tracker_new: 새 일감 유형
+ label_workflow: 업무흐름
+ label_issue_status: 일감 상태
+ label_issue_status_plural: 일감 상태
+ label_issue_status_new: 새 일감 상태
+ label_issue_category: 일감 범주
+ label_issue_category_plural: 일감 범주
+ label_issue_category_new: 새 일감 범주
+ label_custom_field: 사용자 정의 항목
+ label_custom_field_plural: 사용자 정의 항목
+ label_custom_field_new: 새 사용자 정의 항목
+ label_enumerations: 코드값
+ label_enumeration_new: 새 코드값
+ label_information: 정보
+ label_information_plural: 정보
+ label_please_login: 로그인하세요.
+ label_register: 등록
+ label_login_with_open_id_option: 또는 OpenID로 로그인
+ label_password_lost: 비밀번호 찾기
+ label_home: 초기화면
+ label_my_page: 내 페이지
+ label_my_account: 내 계정
+ label_my_projects: 내 프로젝트
+ label_administration: 관리
+ label_login: 로그인
+ label_logout: 로그아웃
+ label_help: 도움말
+ label_reported_issues: 보고한 일감
+ label_assigned_to_me_issues: 내가 맡은 일감
+ label_last_login: 마지막 접속
+ label_registered_on: 등록시각
+ label_activity: 작업내역
+ label_overall_activity: 전체 작업내역
+ label_user_activity: "{{value}}의 작업내역"
+ label_new: 새로 만들기
+ label_logged_as: '로그인계정:'
+ label_environment: 환경
+ label_authentication: 인증
+ label_auth_source: 인증 공급자
+ label_auth_source_new: 새 인증 공급자
+ label_auth_source_plural: 인증 공급자
+ label_subproject_plural: 하위 프로젝트
+ label_and_its_subprojects: "{{value}}와 하위 프로젝트들"
+ label_min_max_length: 최소 - 최대 길이
+ label_list: 목록
+ label_date: 날짜
+ label_integer: 정수
+ label_float: 부동소수
+ label_boolean: 부울린
+ label_string: 문자열
+ label_text: 텍스트
+ label_attribute: 속성
+ label_attribute_plural: 속성
+ label_download: "{{count}}회 다운로드"
+ label_download_plural: "{{count}}회 다운로드"
+ label_no_data: 표시할 데이터가 없습니다.
+ label_change_status: 상태 변경
+ label_history: 이력
+ label_attachment: 파일
+ label_attachment_new: 파일추가
+ label_attachment_delete: 파일삭제
+ label_attachment_plural: 파일
+ label_file_added: 파일 추가
+ label_report: 보고서
+ label_report_plural: 보고서
+ label_news: 뉴스
+ label_news_new: 새 뉴스
+ label_news_plural: 뉴스
+ label_news_latest: 최근 뉴스
+ label_news_view_all: 모든 뉴스
+ label_news_added: 뉴스 추가
+ label_change_log: 변경 이력
+ label_settings: 설정
+ label_overview: 개요
+ label_version: 버전
+ label_version_new: 새 버전
+ label_version_plural: 버전
+ label_confirmation: 확인
+ label_export_to: 내보내기
+ label_read: 읽기...
+ label_public_projects: 공개 프로젝트
+ label_open_issues: 진행중
+ label_open_issues_plural: 진행중
+ label_closed_issues: 완료됨
+ label_closed_issues_plural: 완료됨
+ label_x_open_issues_abbr_on_total:
+ zero: "총 {{total}} 건 모두 완료"
+ one: "한 건 진행 중 / 총 {{total}} 건 중 "
+ other: "{{count}} 건 진행 중 / 총 {{total}} 건"
+ label_x_open_issues_abbr:
+ zero: 모두 완료
+ one: 한 건 진행 중
+ other: "{{count}} 건 진행 중"
+ label_x_closed_issues_abbr:
+ zero: 모두 미완료
+ one: 한 건 완료
+ other: "{{count}} 건 완료"
+ label_total: 합계
+ label_permissions: 권한
+ label_current_status: 일감 상태
+ label_new_statuses_allowed: 허용되는 일감 상태
+ label_all: 모두
+ label_none: 없음
+ label_nobody: 미지정
+ label_next: 다음
+ label_previous: 뒤로
+ label_used_by: 사용됨
+ label_details: 자세히
+ label_add_note: 일감덧글 추가
+ label_per_page: 페이지별
+ label_calendar: 달력
+ label_months_from: 개월 동안 | 다음부터
+ label_gantt: Gantt 챠트
+ label_internal: 내부
+ label_last_changes: "최근 {{count}}개의 변경사항"
+ label_change_view_all: 모든 변경 내역 보기
+ label_personalize_page: 입맛대로 구성하기
+ label_comment: 댓글
+ label_comment_plural: 댓글
+ label_x_comments:
+ zero: 댓글 없음
+ one: 한 개의 댓글
+ other: "{{count}} 개의 댓글"
+ label_comment_add: 댓글 추가
+ label_comment_added: 댓글이 추가되었습니다.
+ label_comment_delete: 댓글 삭제
+ label_query: 검색양식
+ label_query_plural: 검색양식
+ label_query_new: 새 검색양식
+ label_filter_add: 검색조건 추가
+ label_filter_plural: 검색조건
+ label_equals: 이다
+ label_not_equals: 아니다
+ label_in_less_than: 이내
+ label_in_more_than: 이후
+ label_greater_or_equal: ">="
+ label_less_or_equal: "<="
+ label_in: 이내
+ label_today: 오늘
+ label_all_time: 모든 시간
+ label_yesterday: 어제
+ label_this_week: 이번주
+ label_last_week: 지난 주
+ label_last_n_days: "지난 {{count}} 일"
+ label_this_month: 이번 달
+ label_last_month: 지난 달
+ label_this_year: 올해
+ label_date_range: 날짜 범위
+ label_less_than_ago: 이전
+ label_more_than_ago: 이후
+ label_ago: 일 전
+ label_contains: 포함되는 키워드
+ label_not_contains: 포함하지 않는 키워드
+ label_day_plural: 일
+ label_repository: 저장소
+ label_repository_plural: 저장소
+ label_browse: 저장소 살피기
+ label_modification: "{{count}} 변경"
+ label_modification_plural: "{{count}} 변경"
+ label_revision: 개정판
+ label_revision_plural: 개정판
+ label_associated_revisions: 관련된 개정판들
+ label_added: 추가됨
+ label_modified: 변경됨
+ label_copied: 복사됨
+ label_renamed: 이름바뀜
+ label_deleted: 삭제됨
+ label_latest_revision: 최근 개정판
+ label_latest_revision_plural: 최근 개정판
+ label_view_revisions: 개정판 보기
+ label_max_size: 최대 크기
+ label_sort_highest: 맨 위로
+ label_sort_higher: 위로
+ label_sort_lower: 아래로
+ label_sort_lowest: 맨 아래로
+ label_roadmap: 로드맵
+ label_roadmap_due_in: "기한 {{value}}"
+ label_roadmap_overdue: "{{value}} 지연"
+ label_roadmap_no_issues: 이 버전에 해당하는 일감 없음
+ label_search: 검색
+ label_result_plural: 결과
+ label_all_words: 모든 단어
+ label_wiki: 위키
+ label_wiki_edit: 위키 편집
+ label_wiki_edit_plural: 위키 편집
+ label_wiki_page: 위키 페이지
+ label_wiki_page_plural: 위키 페이지
+ label_index_by_title: 제목별 색인
+ label_index_by_date: 날짜별 색인
+ label_current_version: 현재 버전
+ label_preview: 미리보기
+ label_feed_plural: 피드(Feeds)
+ label_changes_details: 모든 상세 변경 내역
+ label_issue_tracking: 일감 추적
+ label_spent_time: 소요 시간
+ label_f_hour: "{{value}} 시간"
+ label_f_hour_plural: "{{value}} 시간"
+ label_time_tracking: 시간추적
+ label_change_plural: 변경사항들
+ label_statistics: 통계
+ label_commits_per_month: 월별 제출 내역
+ label_commits_per_author: 저자별 제출 내역
+ label_view_diff: 차이점 보기
+ label_diff_inline: 한줄로
+ label_diff_side_by_side: 두줄로
+ label_options: 옵션
+ label_copy_workflow_from: 업무흐름 복사하기
+ label_permissions_report: 권한 보고서
+ label_watched_issues: 지켜보고 있는 일감
+ label_related_issues: 연결된 일감
+ label_applied_status: 적용된 상태
+ label_loading: 읽는 중...
+ label_relation_new: 새 관계
+ label_relation_delete: 관계 지우기
+ label_relates_to: "다음 일감과 관련됨:"
+ label_duplicates: "다음 일감과 겹침:"
+ label_duplicated_by: "다음 일감과 겹침:"
+ label_blocks: "다음 일감의 해결을 막고 있음:"
+ label_blocked_by: "다음 일감에게 막혀 있음:"
+ label_precedes: "다음에 진행할 일감:"
+ label_follows: "다음 일감을 우선 진행:"
+ label_end_to_start: end to start
+ label_end_to_end: end to end
+ label_start_to_start: start to start
+ label_start_to_end: start to end
+ label_stay_logged_in: 로그인 유지
+ label_disabled: 비활성화
+ label_show_completed_versions: 완료된 버전 보기
+ label_me: 나
+ label_board: 게시판
+ label_board_new: 새 게시판
+ label_board_plural: 게시판
+ label_topic_plural: 주제
+ label_message_plural: 글
+ label_message_last: 마지막 글
+ label_message_new: 새글쓰기
+ label_message_posted: 글 추가
+ label_reply_plural: 답글
+ label_send_information: 사용자에게 계정정보를 보내기
+ label_year: 년
+ label_month: 월
+ label_week: 주
+ label_date_from: '기간:'
+ label_date_to: ' ~ '
+ label_language_based: 언어설정에 따름
+ label_sort_by: "{{value}}(으)로 정렬"
+ label_send_test_email: 테스트 메일 보내기
+ label_feeds_access_key_created_on: "피드 접근 키가 {{value}} 이전에 생성됨 "
+ label_module_plural: 모듈
+ label_added_time_by: "{{author}}이(가) {{age}} 전에 추가함"
+ label_updated_time_by: "{{author}}이(가) {{age}} 전에 변경"
+ label_updated_time: "{{value}} 전에 수정됨"
+ label_jump_to_a_project: 프로젝트 바로가기
+ label_file_plural: 파일
+ label_changeset_plural: 변경묶음
+ label_default_columns: 기본 컬럼
+ label_no_change_option: (수정 안함)
+ label_bulk_edit_selected_issues: 선택된 일감들을 한꺼번에 수정하기
+ label_theme: 테마
+ label_default: 기본
+ label_search_titles_only: 제목에서만 찾기
+ label_user_mail_option_all: "내가 속한 프로젝트로들부터 모든 메일 받기"
+ label_user_mail_option_selected: "선택한 프로젝트들로부터 모든 메일 받기.."
+ label_user_mail_option_none: "내가 속하거나 감시 중인 사항에 대해서만"
+ label_user_mail_no_self_notified: "내가 만든 변경사항들에 대해서는 알림메일을 받지 않습니다."
+ label_registration_activation_by_email: 메일로 계정을 활성화하기
+ label_registration_automatic_activation: 자동 계정 활성화
+ label_registration_manual_activation: 수동 계정 활성화
+ label_display_per_page: "페이지당: {{value}}"
+ label_age: 마지막 수정일
+ label_change_properties: 속성 변경
+ label_general: 일반
+ label_more: 제목 및 설명 수정
+ label_scm: 형상관리시스템
+ label_plugins: 플러그인
+ label_ldap_authentication: LDAP 인증
+ label_downloads_abbr: D/L
+ label_optional_description: 부가적인 설명
+ label_add_another_file: 다른 파일 추가
+ label_preferences: 설정
+ label_chronological_order: 시간 순으로 정렬
+ label_reverse_chronological_order: 시간 역순으로 정렬
+ label_planning: 프로젝트계획
+ label_incoming_emails: 수신 메일
+ label_generate_key: 키 생성
+ label_issue_watchers: 일감지킴이
+ label_example: 예
+ label_display: 표시방식
+ label_sort: 정렬
+ label_ascending: 오름차순
+ label_descending: 내림차순
+ label_date_from_to: "{{start}}부터 {{end}}까지"
+ label_wiki_content_added: 위키페이지 추가
+ label_wiki_content_updated: 위키페이지 수정
+
+ button_login: 로그인
+ button_submit: 확인
+ button_save: 저장
+ button_check_all: 모두선택
+ button_uncheck_all: 선택해제
+ button_delete: 삭제
+ button_create: 만들기
+ button_create_and_continue: 만들고 계속하기
+ button_test: 테스트
+ button_edit: 편집
+ button_add: 추가
+ button_change: 변경
+ button_apply: 적용
+ button_clear: 지우기
+ button_lock: 잠금
+ button_unlock: 잠금해제
+ button_download: 다운로드
+ button_list: 목록
+ button_view: 보기
+ button_move: 이동
+ button_back: 뒤로
+ button_cancel: 취소
+ button_activate: 활성화
+ button_sort: 정렬
+ button_log_time: 작업시간 기록
+ button_rollback: 이 버전으로 되돌리기
+ button_watch: 지켜보기
+ button_unwatch: 관심끄기
+ button_reply: 답글
+ button_archive: 잠금보관
+ button_unarchive: 잠금보관해제
+ button_reset: 초기화
+ button_rename: 이름바꾸기
+ button_change_password: 비밀번호 바꾸기
+ button_copy: 복사
+ button_annotate: 이력해설
+ button_update: 수정
+ button_configure: 설정
+ button_quote: 댓글달기
+
+ status_active: 사용중
+ status_registered: 등록대기
+ status_locked: 잠김
+
+ text_select_mail_notifications: 알림메일이 필요한 작업을 선택하세요.
+ text_regexp_info: 예) ^[A-Z0-9]+$
+ text_min_max_length_info: 0 는 제한이 없음을 의미함
+ text_project_destroy_confirmation: 이 프로젝트를 삭제하고 모든 데이터를 지우시겠습니까?
+ text_subprojects_destroy_warning: "하위 프로젝트({{value}})이(가) 자동으로 지워질 것입니다."
+ text_workflow_edit: 업무흐름 수정하려면 역할과 일감유형을 선택하세요.
+ text_are_you_sure: 계속 진행 하시겠습니까?
+ text_tip_task_begin_day: 오늘 시작하는 업무(task)
+ text_tip_task_end_day: 오늘 종료하는 업무(task)
+ text_tip_task_begin_end_day: 오늘 시작하고 종료하는 업무(task)
+ text_project_identifier_info: '영문 소문자(a-z) 및 숫자, 대쉬(-) 가능.<br />저장된후에는 식별자 변경 불가능.'
+ text_caracters_maximum: "최대 {{count}} 글자 가능"
+ text_caracters_minimum: "최소한 {{count}} 글자 이상이어야 합니다."
+ text_length_between: "{{min}} 에서 {{max}} 글자"
+ text_tracker_no_workflow: 이 일감 유형에는 업무흐름이 정의되지 않았습니다.
+ text_unallowed_characters: 허용되지 않는 문자열
+ text_comma_separated: "구분자','를 이용해서 여러 개의 값을 입력할 수 있습니다."
+ text_issues_ref_in_commit_messages: 제출 메시지에서 일감을 참조하거나 해결하기
+ text_issue_added: "{{author}}이(가) 일감 {{id}}을(를) 보고하였습니다."
+ text_issue_updated: "{{author}}이(가) 일감 {{id}}을(를) 수정하였습니다."
+ text_wiki_destroy_confirmation: 이 위키와 모든 내용을 지우시겠습니까?
+ text_issue_category_destroy_question: "일부 일감들({{count}}개)이 이 범주에 지정되어 있습니다. 어떻게 하시겠습니까?"
+ text_issue_category_destroy_assignments: 범주 지정 지우기
+ text_issue_category_reassign_to: 일감을 이 범주에 다시 지정하기
+ text_user_mail_option: "선택하지 않은 프로젝트에서도, 지켜보는 중이거나 속해있는 사항(일감을 발행했거나 할당된 경우)이 있으면 알림메일을 받게 됩니다."
+ text_no_configuration_data: "역할, 일감 유형, 일감 상태들과 업무흐름이 아직 설정되지 않았습니다.\n기본 설정을 읽어들이는 것을 권장합니다. 읽어들인 후에 수정할 수 있습니다."
+ text_load_default_configuration: 기본 설정을 읽어들이기
+ text_status_changed_by_changeset: "변경묶음 {{value}}에 의하여 변경됨"
+ text_issues_destroy_confirmation: '선택한 일감을 정말로 삭제하시겠습니까?'
+ text_select_project_modules: '이 프로젝트에서 활성화시킬 모듈을 선택하세요:'
+ text_default_administrator_account_changed: 기본 관리자 계정이 변경
+ text_file_repository_writable: 파일 저장소 쓰기 가능
+ text_plugin_assets_writable: 플러그인 전용 디렉토리가 쓰기 가능
+ text_rmagick_available: RMagick 사용 가능 (선택적)
+ text_destroy_time_entries_question: 삭제하려는 일감에 {{hours}} 시간이 보고되어 있습니다. 어떻게 하시겠습니까?
+ text_destroy_time_entries: 보고된 시간을 삭제하기
+ text_assign_time_entries_to_project: 보고된 시간을 프로젝트에 할당하기
+ text_reassign_time_entries: '이 알림에 보고된 시간을 재할당하기:'
+ text_user_wrote: "{{value}}의 덧글:"
+ text_enumeration_category_reassign_to: '새로운 값을 설정:'
+ text_enumeration_destroy_question: "{{count}} 개의 일감이 이 값을 사용하고 있습니다."
+ text_email_delivery_not_configured: "이메일 전달이 설정되지 않았습니다. 그래서 알림이 비활성화되었습니다.\n SMTP서버를 config/email.yml에서 설정하고 어플리케이션을 다시 시작하십시오. 그러면 동작합니다."
+ text_repository_usernames_mapping: "저장소 로그에서 발견된 각 사용자에 레드마인 사용자를 업데이트할때 선택합니다.\n레드마인과 저장소의 이름이나 이메일이 같은 사용자가 자동으로 연결됩니다."
+ text_diff_truncated: '... 이 차이점은 표시할 수 있는 최대 줄수를 초과해서 이 차이점은 잘렸습니다.'
+ text_custom_field_possible_values_info: '각 값 당 한 줄'
+ text_wiki_page_destroy_question: 이 페이지는 {{descendants}} 개의 하위 페이지와 관련 내용이 있습니다. 이 내용을 어떻게 하시겠습니까?
+ text_wiki_page_nullify_children: 하위 페이지를 최상위 페이지 아래로 지정
+ text_wiki_page_destroy_children: 모든 하위 페이지와 관련 내용을 삭제
+ text_wiki_page_reassign_children: 하위 페이지를 이 페이지 아래로 지정
+
+ default_role_manager: 관리자
+ default_role_developper: 개발자
+ default_role_reporter: 보고자
+ default_tracker_bug: 결함
+ default_tracker_feature: 새기능
+ default_tracker_support: 지원
+ default_issue_status_new: 신규
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: 해결
+ default_issue_status_feedback: 의견
+ default_issue_status_closed: 완료
+ default_issue_status_rejected: 거절
+ default_doc_category_user: 사용자 문서
+ default_doc_category_tech: 기술 문서
+ default_priority_low: 낮음
+ default_priority_normal: 보통
+ default_priority_high: 높음
+ default_priority_urgent: 긴급
+ default_priority_immediate: 즉시
+ default_activity_design: 설계
+ default_activity_development: 개발
+
+ enumeration_issue_priorities: 일감 우선순위
+ enumeration_doc_categories: 문서 범주
+ enumeration_activities: 작업분류(시간추적)
+
+ field_issue_to: Related issue
+ label_view_all_revisions: 모든 개정판 표시
+ label_tag: 표지(票識)저장소
+ label_branch: 분기(分岐)저장소
+ error_no_tracker_in_project: 사용할 수 있도록 설정된 일감 유형이 없습니다. 프로젝트 설정을 확인하십시오.
+ error_no_default_issue_status: '기본 상태가 정해져 있지 않습니다. 설정을 확인하십시오. (주 메뉴의 "관리" -> "일감 상태")'
+ text_journal_changed: "{{label}}을(를) {{old}}에서 {{new}}(으)로 변경함"
+ text_journal_set_to: "{{label}}을(를) {{value}}(으)로 정함"
+ text_journal_deleted: "{{label}}을(를) 지움 ({{old}})"
+ label_group_plural: 그룹
+ label_group: 그룹
+ label_group_new: 새 그룹
+ label_time_entry_plural: 작업시간
+ text_journal_added: "{{label}} {{value}} 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
--- /dev/null
+# Lithuanian translations for Ruby on Rails
+# by Laurynas Butkus (laurynas.butkus@gmail.com)
+
+lt:
+ number:
+ format:
+ separator: ","
+ delimiter: " "
+ precision: 3
+
+ currency:
+ format:
+ format: "%n %u"
+ unit: "Lt"
+ separator: ","
+ delimiter: " "
+ precision: 2
+
+ percentage:
+ format:
+ delimiter: ""
+
+ precision:
+ format:
+ delimiter: ""
+
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "baitai"
+ other: "baitai"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "pusė minutės"
+ less_than_x_seconds:
+ one: "mažiau nei 1 sekundė"
+ other: "mažiau nei {{count}} sekundės"
+ x_seconds:
+ one: "1 sekundė"
+ other: "{{count}} sekundės"
+ less_than_x_minutes:
+ one: "mažiau nei minutė"
+ other: "mažiau nei {{count}} minutės"
+ x_minutes:
+ one: "1 minutė"
+ other: "{{count}} minutės"
+ about_x_hours:
+ one: "apie 1 valanda"
+ other: "apie {{count}} valandų"
+ x_days:
+ one: "1 diena"
+ other: "{{count}} dienų"
+ about_x_months:
+ one: "apie 1 mėnuo"
+ other: "apie {{count}} mėnesiai"
+ x_months:
+ one: "1 mėnuo"
+ other: "{{count}} mėnesiai"
+ about_x_years:
+ one: "apie 1 metai"
+ other: "apie {{count}} metų"
+ over_x_years:
+ one: "virš 1 metų"
+ other: "virš {{count}} metų"
+ prompts:
+ year: "Metai"
+ month: "Mėnuo"
+ day: "Diena"
+ hour: "Valanda"
+ minute: "Minutė"
+ second: "Sekundės"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Išsaugant objektą {{model}} rasta klaida"
+ other: "Išsaugant objektą {{model}} rastos {{count}} klaidos"
+ body: "Šiuose laukuose yra klaidų:"
+
+ messages:
+ inclusion: "nenumatyta reikšmė"
+ exclusion: "užimtas"
+ invalid: "neteisingas"
+ confirmation: "neteisingai pakartotas"
+ accepted: "turi būti patvirtintas"
+ empty: "negali būti tuščias"
+ blank: "negali būti tuščias"
+ too_long: "per ilgas (daugiausiai {{count}} simboliai)"
+ too_short: "per trumpas (mažiausiai {{count}} simboliai)"
+ wrong_length: "neteisingo ilgio (turi būti {{count}} simboliai)"
+ taken: "jau užimtas"
+ not_a_number: "ne skaičius"
+ greater_than: "turi būti didesnis už {{count}}"
+ greater_than_or_equal_to: "turi būti didesnis arba lygus {{count}}"
+ equal_to: "turi būti lygus {{count}}"
+ less_than: "turi būti mažesnis už {{count}}"
+ less_than_or_equal_to: "turi būti mažesnis arba lygus {{count}}"
+ odd: "turi būti nelyginis"
+ even: "turi būti lyginis"
+ greater_than_start_date: "turi būti didesnė negu pradžios data"
+ not_same_project: "nepriklauso tam pačiam projektui"
+ circular_dependency: "Šis ryšys sukurtų ciklinę priklausomybę"
+
+ models:
+
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [sekmadienis, pirmadienis, antradienis, trečiadienis, ketvirtadienis, penktadienis, šeštadienis]
+ abbr_day_names: [Sek, Pir, Ant, Tre, Ket, Pen, Šeš]
+
+ month_names: [~, sausio, vasario, kovo, balandžio, gegužės, birželio, liepos, rugpjūčio, rugsėjo, spalio, lapkričio, gruodžio]
+ abbr_month_names: [~, Sau, Vas, Kov, Bal, Geg, Bir, Lie, Rgp, Rgs, Spa, Lap, Grd]
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: " ir "
+ last_word_connector: " ir "
+
+
+ actionview_instancetag_blank_option: prašom išrinkti
+
+ general_text_No: 'Ne'
+ general_text_Yes: 'Taip'
+ general_text_no: 'ne'
+ general_text_yes: 'taip'
+ general_lang_name: 'Lithuanian (lietuvių)'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Paskyra buvo sėkmingai atnaujinta.
+ notice_account_invalid_creditentials: Negaliojantis vartotojo vardas ar slaptažodis
+ notice_account_password_updated: Slaptažodis buvo sėkmingai atnaujintas.
+ notice_account_wrong_password: Neteisingas slaptažodis
+ notice_account_register_done: Paskyra buvo sėkmingai sukurta. Kad aktyvintumėte savo paskyrą, paspauskite sąsają, kuri jums buvo siųsta elektroniniu paštu.
+ notice_account_unknown_email: Nežinomas vartotojas.
+ notice_can_t_change_password: Šis pranešimas naudoja išorinį autentiškumo nustatymo šaltinį. Neįmanoma pakeisti slaptažodį.
+ notice_account_lost_email_sent: Į Jūsų pašą išsiūstas laiškas su naujo slaptažodžio pasirinkimo instrukcija.
+ notice_account_activated: Jūsų paskyra aktyvuota. Galite prisijungti.
+ notice_successful_create: Sėkmingas sukūrimas.
+ notice_successful_update: Sėkmingas atnaujinimas.
+ notice_successful_delete: Sėkmingas panaikinimas.
+ notice_successful_connection: Sėkmingas susijungimas.
+ notice_file_not_found: Puslapis, į kurį ketinate įeiti, neegzistuoja arba pašalintas.
+ notice_locking_conflict: Duomenys atnaujinti kito vartotojo.
+ notice_not_authorized: Jūs neturite teisių gauti prieigą prie šio puslapio.
+ notice_email_sent: "Laiškas išsiųstas {{value}}"
+ notice_email_error: "Laiško siųntimo metu įvyko klaida ({{value}})"
+ notice_feeds_access_key_reseted: Jūsų RSS raktas buvo atnaujintas.
+ notice_failed_to_save_issues: "Nepavyko išsaugoti {{count}} problemos(ų) iš {{total}} pasirinkto: {{ids}}."
+ notice_no_issue_selected: "Nepasirinkta nė viena problema! Prašom pažymėti problemą, kurią norite redaguoti."
+ notice_account_pending: "Jūsų paskyra buvo sukūrta ir dabar laukiama administratoriaus patvirtinimo."
+ notice_default_data_loaded: Numatytoji konfiguracija sėkmingai užkrauta.
+ notice_unable_delete_version: Neimanoma panaikinti versiją
+
+ error_can_t_load_default_data: "Numatytoji konfiguracija negali būti užkrauta: {{value}}"
+ error_scm_not_found: "Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja."
+ error_scm_command_failed: "Įvyko klaida jungiantis prie saugyklos: {{value}}"
+ error_scm_annotate: "Įrašas neegzituoja arba negalima jo atvaizduoti."
+ error_issue_not_found_in_project: 'Darbas nerastas arba nesurištas su šiuo projektu'
+
+ mail_subject_lost_password: "Jūsų {{value}} slaptažodis"
+ mail_body_lost_password: 'Norėdami pakeisti slaptažodį, spauskite nuorodą:'
+ mail_subject_register: "{{value}} paskyros aktyvavymas"
+ mail_body_register: 'Norėdami aktyvuoti paskyrą, spauskite nuorodą:'
+ mail_body_account_information_external: "Jūs galite naudoti Jūsų {{value}} paskyrą, norėdami prisijungti."
+ mail_body_account_information: Informacija apie Jūsų paskyrą
+ mail_subject_account_activation_request: "{{value}} paskyros aktyvavimo prašymas"
+ mail_body_account_activation_request: "Užsiregistravo naujas vartotojas ({{value}}). Jo paskyra laukia jūsų patvirtinimo:"
+ mail_subject_reminder: "{{count}} darbas(ai) po kelių dienų"
+ mail_body_reminder: "{{count}} darbas(ai), kurie yra jums priskirti, baigiasi po {{days}} dienų(os):"
+
+ gui_validation_error: 1 klaida
+ gui_validation_error_plural: "{{count}} klaidų(os)"
+
+ field_name: Pavadinimas
+ field_description: Aprašas
+ field_summary: Santrauka
+ field_is_required: Reikalaujama
+ field_firstname: Vardas
+ field_lastname: Pavardė
+ field_mail: Email
+ field_filename: Byla
+ field_filesize: Dydis
+ field_downloads: Atsiuntimai
+ field_author: Autorius
+ field_created_on: Sukūrta
+ field_updated_on: Atnaujinta
+ field_field_format: Formatas
+ field_is_for_all: Visiems projektams
+ field_possible_values: Galimos reikšmės
+ field_regexp: Pastovi išraiška
+ field_min_length: Minimalus ilgis
+ field_max_length: Maksimalus ilgis
+ field_value: Vertė
+ field_category: Kategorija
+ field_title: Pavadinimas
+ field_project: Projektas
+ field_issue: Darbas
+ field_status: Būsena
+ field_notes: Pastabos
+ field_is_closed: Darbas uždarytas
+ field_is_default: Numatytoji vertė
+ field_tracker: Pėdsekys
+ field_subject: Tema
+ field_due_date: Užbaigimo data
+ field_assigned_to: Paskirtas
+ field_priority: Prioritetas
+ field_fixed_version: Tikslinė versija
+ field_user: Vartotojas
+ field_role: Vaidmuo
+ field_homepage: Pagrindinis puslapis
+ field_is_public: Viešas
+ field_parent: Priklauso projektui
+ field_is_in_chlog: Darbai rodomi pokyčių žurnale
+ field_is_in_roadmap: Darbai rodomi veiklos grafike
+ field_login: Registracijos vardas
+ field_mail_notification: Elektroninio pašto pranešimai
+ field_admin: Administratorius
+ field_last_login_on: Paskutinis ryšys
+ field_language: Kalba
+ field_effective_date: Data
+ field_password: Slaptažodis
+ field_new_password: Naujas slaptažodis
+ field_password_confirmation: Patvirtinimas
+ field_version: Versija
+ field_type: Tipas
+ field_host: Pagrindinis kompiuteris
+ field_port: Portas
+ field_account: Paskyra
+ field_base_dn: Bazinis skiriamasis vardas
+ field_attr_login: Registracijos vardo požymis
+ field_attr_firstname: Vardo priskiria
+ field_attr_lastname: Pavardės priskiria
+ field_attr_mail: Elektroninio pašto požymis
+ field_onthefly: Automatinis vartotojų registravimas
+ field_start_date: Pradėti
+ field_done_ratio: % Atlikta
+ field_auth_source: Autentiškumo nustatymo būdas
+ field_hide_mail: Paslėpkite mano elektroninio pašto adresą
+ field_comments: Komentaras
+ field_url: URL
+ field_start_page: Pradžios puslapis
+ field_subproject: Subprojektas
+ field_hours: Valandos
+ field_activity: Veikla
+ field_spent_on: Data
+ field_identifier: Identifikuotojas
+ field_is_filter: Panaudotas kaip filtras
+ field_issue_to: Susijęs darbas
+ field_delay: Užlaikymas
+ field_assignable: Darbai gali būti paskirti šiam vaidmeniui
+ field_redirect_existing_links: Peradresuokite egzistuojančias sąsajas
+ field_estimated_hours: Numatyta trukmė
+ field_column_names: Skiltys
+ field_time_zone: Laiko juosta
+ field_searchable: Randamas
+ field_default_value: Numatytoji vertė
+ field_comments_sorting: rodyti komentarus
+ field_parent_title: Aukštesnio lygio puslapis
+
+ setting_app_title: Programos pavadinimas
+ setting_app_subtitle: Programos paantraštė
+ setting_welcome_text: Pasveikinimas
+ setting_default_language: Numatytoji kalba
+ setting_login_required: Reikalingas autentiškumo nustatymas
+ setting_self_registration: Saviregistracija
+ setting_attachment_max_size: Priedo maks. dydis
+ setting_issues_export_limit: Darbų eksportavimo riba
+ setting_mail_from: Emisijos elektroninio pašto adresas
+ setting_bcc_recipients: Akli tikslios kopijos gavėjai (bcc)
+ setting_plain_text_mail: tik grinas tekstas (be HTML)
+ setting_host_name: Pagrindinio kompiuterio vardas
+ setting_text_formatting: Teksto apipavidalinimas
+ setting_wiki_compression: Wiki istorijos suspaudimas
+ setting_feeds_limit: Perdavimo turinio riba
+ setting_default_projects_public: Naujas projektas viešas pagal nutylėjimą
+ setting_autofetch_changesets: Automatinis pakeitimų siuntimas
+ setting_sys_api_enabled: Įgalinkite WS sandėlio vadybai
+ setting_commit_ref_keywords: Nurodymo reikšminiai žodžiai
+ setting_commit_fix_keywords: Fiksavimo reikšminiai žodžiai
+ setting_autologin: Autoregistracija
+ setting_date_format: Datos formatas
+ setting_time_format: Laiko formatas
+ setting_cross_project_issue_relations: Leisti tarprojektinius darbų ryšius
+ setting_issue_list_default_columns: Numatytosios skiltys darbų sąraše
+ setting_repositories_encodings: Saugyklos koduotė
+ setting_commit_logs_encoding: Commit pranėšimų koduotė
+ setting_emails_footer: elektroninio pašto puslapinė poraštė
+ setting_protocol: Protokolas
+ setting_per_page_options: Įrašų puslapyje nustatimas
+ setting_user_format: Vartotojo atvaizdavimo formatas
+ setting_activity_days_default: Atvaizduojamos dienos projekto veikloje
+ setting_display_subprojects_issues: Pagal nutylėjimą rodyti subprojektų darbus pagrindiniame projekte
+ setting_enabled_scm: Įgalintas SCM
+ setting_mail_handler_api_enabled: Įgalinti WS įeinantiems laiškams
+ setting_mail_handler_api_key: API raktas
+ setting_sequential_project_identifiers: Generuoti nuoseklus projekto identifikatorius
+ setting_gravatar_enabled: Naudoti Gravatar vartotojo ikonkės
+ setting_diff_max_lines_displayed: Maksimalus rodomas eilučiu skaičius diff\'e
+
+ permission_edit_project: Edit project
+ permission_select_project_modules: Select project modules
+ permission_manage_members: Manage members
+ permission_manage_versions: Manage versions
+ permission_manage_categories: Manage issue categories
+ permission_add_issues: Add issues
+ permission_edit_issues: Edit issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_add_issue_notes: Add notes
+ permission_edit_issue_notes: Edit notes
+ permission_edit_own_issue_notes: Edit own notes
+ permission_move_issues: Move issues
+ permission_delete_issues: Delete issues
+ permission_manage_public_queries: Manage public queries
+ permission_save_queries: Save queries
+ permission_view_gantt: View gantt chart
+ permission_view_calendar: View calendar
+ permission_view_issue_watchers: View watchers list
+ permission_add_issue_watchers: Add watchers
+ permission_log_time: Log spent time
+ permission_view_time_entries: View spent time
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_news: Manage news
+ permission_comment_news: Comment news
+ permission_manage_documents: Manage documents
+ permission_view_documents: View documents
+ permission_manage_files: Manage files
+ permission_view_files: View files
+ permission_manage_wiki: Manage wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_view_wiki_pages: View wiki
+ permission_view_wiki_edits: View wiki history
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_manage_repository: Manage repository
+ permission_browse_repository: Browse repository
+ permission_view_changesets: View changesets
+ permission_commit_access: Commit access
+ permission_manage_boards: Manage boards
+ permission_view_messages: View messages
+ permission_add_messages: Post messages
+ permission_edit_messages: Edit messages
+ permission_edit_own_messages: Edit own messages
+ permission_delete_messages: Delete messages
+ permission_delete_own_messages: Delete own messages
+
+ project_module_issue_tracking: Darbu pėdsekys
+ project_module_time_tracking: Laiko pėdsekys
+ project_module_news: Žinios
+ project_module_documents: Dokumentai
+ project_module_files: Rinkmenos
+ project_module_wiki: Wiki
+ project_module_repository: Saugykla
+ project_module_boards: Forumai
+
+ label_user: Vartotojas
+ label_user_plural: Vartotojai
+ label_user_new: Naujas vartotojas
+ label_project: Projektas
+ label_project_new: Naujas projektas
+ label_project_plural: Projektai
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Visi Projektai
+ label_project_latest: Paskutiniai projektai
+ label_issue: Darbas
+ label_issue_new: Naujas darbas
+ label_issue_plural: Darbai
+ label_issue_view_all: Peržiūrėti visus darbus
+ label_issues_by: "Darbai pagal {{value}}"
+ label_issue_added: Darbas pridėtas
+ label_issue_updated: Darbas atnaujintas
+ label_document: Dokumentas
+ label_document_new: Naujas dokumentas
+ label_document_plural: Dokumentai
+ label_document_added: Dokumentas pridėtas
+ label_role: Vaidmuo
+ label_role_plural: Vaidmenys
+ label_role_new: Naujas vaidmuo
+ label_role_and_permissions: Vaidmenys ir leidimai
+ label_member: Narys
+ label_member_new: Naujas narys
+ label_member_plural: Nariai
+ label_tracker: Pėdsekys
+ label_tracker_plural: Pėdsekiai
+ label_tracker_new: Naujas pėdsekys
+ label_workflow: Darbų eiga
+ label_issue_status: Darbo padėtis
+ label_issue_status_plural: Darbų padėtys
+ label_issue_status_new: Nauja padėtis
+ label_issue_category: Darbo kategorija
+ label_issue_category_plural: Darbo kategorijos
+ label_issue_category_new: Nauja kategorija
+ label_custom_field: Kliento laukas
+ label_custom_field_plural: Kliento laukai
+ label_custom_field_new: Naujas kliento laukas
+ label_enumerations: Išvardinimai
+ label_enumeration_new: Nauja vertė
+ label_information: Informacija
+ label_information_plural: Informacija
+ label_please_login: Prašom prisijungti
+ label_register: Užsiregistruoti
+ label_password_lost: Prarastas slaptažodis
+ label_home: Pagrindinis
+ label_my_page: Mano puslapis
+ label_my_account: Mano paskyra
+ label_my_projects: Mano projektai
+ label_administration: Administravimas
+ label_login: Prisijungti
+ label_logout: Atsijungti
+ label_help: Pagalba
+ label_reported_issues: Pranešti darbai
+ label_assigned_to_me_issues: Darbai, priskirti man
+ label_last_login: Paskutinis ryšys
+ label_registered_on: Užregistruota
+ label_activity: Veikla
+ label_overall_activity: Visa veikla
+ label_user_activity: "{{value}}o veiksmai"
+ label_new: Naujas
+ label_logged_as: Prisijungęs kaip
+ label_environment: Aplinka
+ label_authentication: Autentiškumo nustatymas
+ label_auth_source: Autentiškumo nustatymo būdas
+ label_auth_source_new: Naujas autentiškumo nustatymo būdas
+ label_auth_source_plural: Autentiškumo nustatymo būdai
+ label_subproject_plural: Subprojektai
+ label_and_its_subprojects: "{{value}} projektas ir jo subprojektai"
+ label_min_max_length: Min - Maks ilgis
+ label_list: Sąrašas
+ label_date: Data
+ label_integer: Sveikasis skaičius
+ label_float: Float
+ label_boolean: Boolean
+ label_string: Tekstas
+ label_text: Ilgas tekstas
+ label_attribute: Požymis
+ label_attribute_plural: Požymiai
+ label_download: "{{count}} Persiuntimas"
+ label_download_plural: "{{count}} Persiuntimai"
+ label_no_data: Nėra ką atvaizduoti
+ label_change_status: Pakeitimo padėtis
+ label_history: Istorija
+ label_attachment: Rinkmena
+ label_attachment_new: Nauja rinkmena
+ label_attachment_delete: Pašalinkite rinkmeną
+ label_attachment_plural: Rinkmenos
+ label_file_added: Byla pridėta
+ label_report: Ataskaita
+ label_report_plural: Ataskaitos
+ label_news: Žinia
+ label_news_new: Pridėkite žinią
+ label_news_plural: Žinios
+ label_news_latest: Paskutinės naujienos
+ label_news_view_all: Peržiūrėti visas žinias
+ label_news_added: Naujiena pridėta
+ label_change_log: Pakeitimų žurnalas
+ label_settings: Nustatymai
+ label_overview: Apžvalga
+ label_version: Versija
+ label_version_new: Nauja versija
+ label_version_plural: Versijos
+ label_confirmation: Patvirtinimas
+ label_export_to: Eksportuoti į
+ label_read: Skaitykite...
+ label_public_projects: Vieši projektai
+ label_open_issues: atidaryta
+ label_open_issues_plural: atidarytos
+ label_closed_issues: uždaryta
+ label_closed_issues_plural: uždarytos
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Bendra suma
+ label_permissions: Leidimai
+ label_current_status: Einamoji padėtis
+ label_new_statuses_allowed: Naujos padėtys galimos
+ label_all: visi
+ label_none: niekas
+ label_nobody: niekas
+ label_next: Kitas
+ label_previous: Ankstesnis
+ label_used_by: Naudotas
+ label_details: Detalės
+ label_add_note: Pridėkite pastabą
+ label_per_page: Per puslapį
+ label_calendar: Kalendorius
+ label_months_from: mėnesiai nuo
+ label_gantt: Gantt
+ label_internal: Vidinis
+ label_last_changes: "paskutiniai {{count}}, pokyčiai"
+ label_change_view_all: Peržiūrėti visus pakeitimus
+ label_personalize_page: Suasmeninti šį puslapį
+ label_comment: Komentaras
+ label_comment_plural: Komentarai
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Pridėkite komentarą
+ label_comment_added: Komentaras pridėtas
+ label_comment_delete: Pašalinkite komentarus
+ label_query: Užklausa
+ label_query_plural: Užklausos
+ label_query_new: Nauja užklausa
+ label_filter_add: Pridėti filtrą
+ label_filter_plural: Filtrai
+ label_equals: yra
+ label_not_equals: nėra
+ label_in_less_than: mažiau negu
+ label_in_more_than: daugiau negu
+ label_in: in
+ label_today: šiandien
+ label_all_time: visas laikas
+ label_yesterday: vakar
+ label_this_week: šią savaitę
+ label_last_week: paskutinė savaitė
+ label_last_n_days: "paskutinių {{count}} dienų"
+ label_this_month: šis menuo
+ label_last_month: paskutinis menuo
+ label_this_year: šiemet
+ label_date_range: Dienų diapazonas
+ label_less_than_ago: mažiau negu dienomis prieš
+ label_more_than_ago: daugiau negu dienomis prieš
+ label_ago: dienomis prieš
+ label_contains: turi savyje
+ label_not_contains: neturi savyje
+ label_day_plural: dienos
+ label_repository: Saugykla
+ label_repository_plural: Saugiklos
+ label_browse: Naršyti
+ label_modification: "{{count}} pakeitimas"
+ label_modification_plural: "{{count}} pakeitimai"
+ label_revision: Revizija
+ label_revision_plural: Revizijos
+ label_associated_revisions: susijusios revizijos
+ label_added: pridėtas
+ label_modified: pakeistas
+ label_copied: nukopijuotas
+ label_renamed: pervardintas
+ label_deleted: pašalintas
+ label_latest_revision: Paskutinė revizija
+ label_latest_revision_plural: Paskutinės revizijos
+ label_view_revisions: Pežiūrėti revizijas
+ label_max_size: Maksimalus dydis
+ label_sort_highest: Perkelti į viršūnę
+ label_sort_higher: Perkelti į viršų
+ label_sort_lower: Perkelti žemyn
+ label_sort_lowest: Perkelti į apačią
+ label_roadmap: Veiklos grafikas
+ label_roadmap_due_in: "Baigiasi po {{value}}"
+ label_roadmap_overdue: "{{value}} vėluojama"
+ label_roadmap_no_issues: Jokio darbo šiai versijai nėra
+ label_search: Ieškoti
+ label_result_plural: Rezultatai
+ label_all_words: Visi žodžiai
+ label_wiki: Wiki
+ label_wiki_edit: Wiki redakcija
+ label_wiki_edit_plural: Wiki redakcijos
+ label_wiki_page: Wiki puslapis
+ label_wiki_page_plural: Wiki puslapiai
+ label_index_by_title: Indeksas prie pavadinimo
+ label_index_by_date: Indeksas prie datos
+ label_current_version: Einamoji versija
+ label_preview: Peržiūra
+ label_feed_plural: Įeitys(Feeds)
+ label_changes_details: Visų pakeitimų detalės
+ label_issue_tracking: Darbų sekimas
+ label_spent_time: Sugaištas laikas
+ label_f_hour: "{{value}} valanda"
+ label_f_hour_plural: "{{value}} valandų"
+ label_time_tracking: Laiko sekimas
+ label_change_plural: Pakeitimai
+ label_statistics: Statistika
+ label_commits_per_month: Paveda(commit) per mėnesį
+ label_commits_per_author: Autoriaus pavedos(commit)
+ label_view_diff: Skirtumų peržiūra
+ label_diff_inline: įterptas
+ label_diff_side_by_side: šalia
+ label_options: Pasirinkimai
+ label_copy_workflow_from: Kopijuoti darbų eiga iš
+ label_permissions_report: Leidimų pranešimas
+ label_watched_issues: Stebimi darbai
+ label_related_issues: Susiję darbai
+ label_applied_status: Taikomoji padėtis
+ label_loading: Kraunama...
+ label_relation_new: Naujas ryšys
+ label_relation_delete: Pašalinkite ryšį
+ label_relates_to: susietas su
+ label_duplicates: dubliuoja
+ label_duplicated_by: dubliuojasi
+ label_blocks: blokuoja
+ label_blocked_by: blokuojasi
+ label_precedes: ankstesnė
+ label_follows: seka
+ label_end_to_start: užbaigti, kad pradėti
+ label_end_to_end: užbaigti, kad pabaigti
+ label_start_to_start: pradėkite pradėti
+ label_start_to_end: pradėkite užbaigti
+ label_stay_logged_in: Likti prisijungus
+ label_disabled: išjungta(as)
+ label_show_completed_versions: Parodyti užbaigtas versijas
+ label_me: aš
+ label_board: Forumas
+ label_board_new: Naujas forumas
+ label_board_plural: Forumai
+ label_topic_plural: Temos
+ label_message_plural: Pranešimai
+ label_message_last: Paskutinis pranešimas
+ label_message_new: Naujas pranešimas
+ label_message_posted: Pranešimas pridėtas
+ label_reply_plural: Atsakymai
+ label_send_information: Nusiųsti paskyros informaciją vartotojui
+ label_year: Metai
+ label_month: Mėnuo
+ label_week: Savaitė
+ label_date_from: Nuo
+ label_date_to: Iki
+ label_language_based: Pagrįsta vartotojo kalba
+ label_sort_by: "Rūšiuoti pagal {{value}}"
+ label_send_test_email: Nusiųsti bandomąjį elektroninį laišką
+ label_feeds_access_key_created_on: "RSS prieigos raktas sukūrtas prieš {{value}}"
+ label_module_plural: Moduliai
+ label_added_time_by: "Pridėjo {{author}} prieš {{age}}"
+ label_updated_time_by: "Atnaujino {{author}} {{age}} atgal"
+ label_updated_time: "Atnaujinta prieš {{value}}"
+ label_jump_to_a_project: Šuolis į projektą...
+ label_file_plural: Bylos
+ label_changeset_plural: Changesets
+ label_default_columns: Numatyti stulpeliai
+ label_no_change_option: (Jokio pakeitimo)
+ label_bulk_edit_selected_issues: Masinis pasirinktų darbų(issues) redagavimas
+ label_theme: Tema
+ label_default: Numatyta(as)
+ label_search_titles_only: Ieškoti pavadinimų tiktai
+ label_user_mail_option_all: "Bet kokiam įvykiui visuose mano projektuose"
+ label_user_mail_option_selected: "Bet kokiam įvykiui tiktai pasirinktuose projektuose ..."
+ label_user_mail_option_none: "Tiktai dalykai kuriuos aš stebiu ar aš esu įtrauktas į"
+ label_user_mail_no_self_notified: "Nenoriu būti informuotas apie pakeitimus, kuriuos pats atlieku"
+ label_registration_activation_by_email: "paskyros aktyvacija per e-paštą"
+ label_registration_manual_activation: "rankinė paskyros aktyvacija"
+ label_registration_automatic_activation: "automatinė paskyros aktyvacija"
+ label_display_per_page: "{{value}} įrašų puslapyje"
+ label_age: Amžius
+ label_change_properties: Pakeisti nustatymus
+ label_general: Bendri
+ label_more: Daugiau
+ label_scm: SCM
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP autentifikacija
+ label_downloads_abbr: siunt.
+ label_optional_description: Apibūdinimas (laisvai pasirenkamas)
+ label_add_another_file: Pridėti kitą bylą
+ label_preferences: Savybės
+ label_chronological_order: Chronologine tvarka
+ label_reverse_chronological_order: Atbuline chronologine tvarka
+ label_planning: Planavimas
+ label_incoming_emails: Įeinantys laiškai
+ label_generate_key: Generuoti raktą
+ label_issue_watchers: Stebėtojai
+ label_example: Pavizdys
+
+ button_login: Registruotis
+ button_submit: Pateikti
+ button_save: Išsaugoti
+ button_check_all: Žymėti visus
+ button_uncheck_all: Atžymėti visus
+ button_delete: Trinti
+ button_create: Sukurti
+ button_test: Testas
+ button_edit: Redaguoti
+ button_add: Pridėti
+ button_change: Keisti
+ button_apply: Pritaikyti
+ button_clear: Išvalyti
+ button_lock: Rakinti
+ button_unlock: Atrakinti
+ button_download: Atsisiųsti
+ button_list: Sąrašas
+ button_view: Žiūrėti
+ button_move: Perkelti
+ button_back: Atgal
+ button_cancel: Atšaukti
+ button_activate: Aktyvinti
+ button_sort: Rūšiuoti
+ button_log_time: Praleistas laikas
+ button_rollback: Grįžti į šią versiją
+ button_watch: Stebėti
+ button_unwatch: Nestebėti
+ button_reply: Atsakyti
+ button_archive: Archyvuoti
+ button_unarchive: Išpakuoti
+ button_reset: Reset
+ button_rename: Pervadinti
+ button_change_password: Pakeisti slaptažodį
+ button_copy: Kopijuoti
+ button_annotate: Rašyti pastabą
+ button_update: Atnaujinti
+ button_configure: Konfigūruoti
+ button_quote: Cituoti
+
+ status_active: aktyvus
+ status_registered: užregistruotas
+ status_locked: užrakintas
+
+ text_select_mail_notifications: Išrinkite veiksmus, apie kuriuos būtų pranešta elektroniniu paštu.
+ text_regexp_info: pvz. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 reiškia jokių apribojimų
+ text_project_destroy_confirmation: Ar esate įsitikinęs, kad jūs norite pašalinti šį projektą ir visus susijusius duomenis?
+ text_subprojects_destroy_warning: "Šis(ie) subprojektas(ai): {{value}} taip pat bus ištrintas(i)."
+ text_workflow_edit: Išrinkite vaidmenį ir pėdsekį, kad redaguotumėte darbų eigą
+ text_are_you_sure: Ar esate įsitikinęs?
+ text_tip_task_begin_day: užduotis, prasidedanti šią dieną
+ text_tip_task_end_day: užduotis, pasibaigianti šią dieną
+ text_tip_task_begin_end_day: užduotis, prasidedanti ir pasibaigianti šią dieną
+ text_project_identifier_info: 'Mažosios raidės (a-z), skaičiai ir brūkšniai galimi.<br/>Išsaugojus, identifikuotojas negali būti keičiamas.'
+ text_caracters_maximum: "{{count}} simbolių maksimumas."
+ text_caracters_minimum: "Turi būti mažiausiai {{count}} simbolių ilgio."
+ text_length_between: "Ilgis tarp {{min}} ir {{max}} simbolių."
+ text_tracker_no_workflow: Jokia darbų eiga neapibrėžta šiam pėdsekiui
+ text_unallowed_characters: Neleistini simboliai
+ text_comma_separated: Leistinos kelios reikšmės (atskirtos kableliu).
+ text_issues_ref_in_commit_messages: Darbų pavedimų(commit) nurodymas ir fiksavimas pranešimuose
+ text_issue_added: "Darbas {{id}} buvo praneštas (by {{author}})."
+ text_issue_updated: "Darbas {{id}} buvo atnaujintas (by {{author}})."
+ text_wiki_destroy_confirmation: Ar esate įsitikinęs, kad jūs norite pašalinti wiki ir visą jos turinį?
+ text_issue_category_destroy_question: "Kai kurie darbai ({{count}}) yra paskirti šiai kategorijai. Ką jūs norite daryti?"
+ text_issue_category_destroy_assignments: Pašalinti kategorijos užduotis
+ text_issue_category_reassign_to: Iš naujo priskirti darbus šiai kategorijai
+ text_user_mail_option: "neišrinktiems projektams, jūs tiktai gausite pranešimus apie įvykius, kuriuos jūs stebite, arba į kuriuos esate įtrauktas (pvz. darbai, jūs esate autorius ar įgaliotinis)."
+ text_no_configuration_data: "Vaidmenys, pėdsekiai, darbų būsenos ir darbų eiga dar nebuvo konfigūruoti.\nGriežtai rekomenduojam užkrauti numatytąją(default)konfiguraciją. Užkrovus, galėsite ją modifikuoti."
+ text_load_default_configuration: Užkrauti numatytąj konfiguraciją
+ text_status_changed_by_changeset: "Pakeista {{value}} revizijoi."
+ text_issues_destroy_confirmation: 'Ar jūs tikrai norite panaikinti pažimėtą(us) darbą(us)?'
+ text_select_project_modules: 'Parinkite modulius, kuriuos norite naudoti šiame projekte:'
+ text_default_administrator_account_changed: Administratoriaus numatyta paskyra pakeista
+ text_file_repository_writable: Į rinkmenu saugyklą galima saugoti (RW)
+ text_rmagick_available: RMagick pasiekiamas (pasirinktinai)
+ text_destroy_time_entries_question: Naikinamam darbui paskelbta {{hours}} valandų. Ką jūs noryte su jomis daryti?
+ text_destroy_time_entries: Ištrinti paskelbtas valandas
+ text_assign_time_entries_to_project: Priskirti valandas prie projekto
+ text_reassign_time_entries: 'Priskirti paskelbtas valandas šiam darbui:'
+ text_user_wrote: "{{value}} parašė:"
+ text_enumeration_destroy_question: "{{count}} objektai priskirti šiai reikšmei."
+ text_enumeration_category_reassign_to: 'Priskirti juos šiai reikšmei:'
+ text_email_delivery_not_configured: "Email pristatymas nesukonfigūruotas , ir perspėjimai neaktyvus.\nSukonfigūruokyte savo SMTP serverį byloje config/email.yml ir perleiskyte programą kad pritaikyti pakeitymus."
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ text_diff_truncated: "... Šis diff'as nukarpitas, todėl kad jis viršijo maksimalu rodoma eilučiu skaičiu."
+
+ default_role_manager: Vadovas
+ default_role_developper: Projektuotojas
+ default_role_reporter: Pranešėjas
+ default_tracker_bug: Klaida
+ default_tracker_feature: Ypatybė
+ default_tracker_support: Palaikymas
+ default_issue_status_new: Nauja
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Išspręsta
+ default_issue_status_feedback: Grįžtamasis ryšys
+ default_issue_status_closed: Uždaryta
+ default_issue_status_rejected: Atmesta
+ default_doc_category_user: Vartotojo dokumentacija
+ default_doc_category_tech: Techniniai dokumentacija
+ default_priority_low: Žemas
+ default_priority_normal: Normalus
+ default_priority_high: Aukštas
+ default_priority_urgent: Skubus
+ default_priority_immediate: Neatidėliotinas
+ default_activity_design: Projektavimas
+ default_activity_development: Vystymas
+
+ enumeration_issue_priorities: Darbo prioritetai
+ enumeration_doc_categories: Dokumento kategorijos
+ enumeration_activities: Veiklos (laiko sekimas)
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+nl:
+ 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: [Zondag, Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag]
+ abbr_day_names: [Zo, Ma, Di, Woe, Do, Vr, Zat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Januari, Februari, Maart, April, Mei, Juni, Juli, Augustus, September, Oktober, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, Mei, Jun, Jul, Aug, Sep, Okt, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "halve minuut"
+ less_than_x_seconds:
+ one: "minder dan een seconde"
+ other: "mindera dan {{count}} seconden"
+ x_seconds:
+ one: "1 seconde"
+ other: "{{count}} seconden"
+ less_than_x_minutes:
+ one: "minder dan een minuut"
+ other: "minder dan {{count}} minuten"
+ x_minutes:
+ one: "1 minuut"
+ other: "{{count}} minuten"
+ about_x_hours:
+ one: "ongeveer 1 uur"
+ other: "ongeveer {{count}} uren"
+ x_days:
+ one: "1 dag"
+ other: "{{count}} dagen"
+ about_x_months:
+ one: "ongeveer 1 maand"
+ other: "ongeveer {{count}} maanden"
+ x_months:
+ one: "1 maand"
+ other: "{{count}} maanden"
+ about_x_years:
+ one: "ongeveer 1 jaar"
+ other: "ongeveer {{count}} jaren"
+ over_x_years:
+ one: "over 1 jaar"
+ other: "over {{count}} jaren"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "en"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "staat niet in de lijst"
+ exclusion: "is gereserveerd"
+ invalid: "is ongeldig"
+ confirmation: "komt niet overeen met bevestiging"
+ accepted: "moet geaccepteerd worden"
+ empty: "mag niet leeg zijn"
+ blank: "mag niet blanco zijn"
+ too_long: "is te lang"
+ too_short: "is te kort"
+ wrong_length: "heeft een onjuiste lengte"
+ taken: "is al in gebruik"
+ not_a_number: "is geen getal"
+ not_a_date: "is geen valide datum"
+ greater_than: "moet groter zijn dan {{count}}"
+ greater_than_or_equal_to: "moet groter zijn of gelijk zijn aan {{count}}"
+ equal_to: "moet gelijk zijn aan {{count}}"
+ less_than: "moet minder zijn dan {{count}}"
+ less_than_or_equal_to: "moet minder dan of gelijk zijn aan {{count}}"
+ odd: "moet oneven zijn"
+ even: "moet even zijn"
+ greater_than_start_date: "moet na de startdatum liggen"
+ not_same_project: "hoort niet bij hetzelfde project"
+ circular_dependency: "Deze relatie zou een circulaire afhankelijkheid tot gevolg hebben"
+
+ actionview_instancetag_blank_option: Selecteer
+
+ button_activate: Activeer
+ button_add: Voeg toe
+ button_annotate: Annoteer
+ button_apply: Pas toe
+ button_archive: Archiveer
+ button_back: Terug
+ button_cancel: Annuleer
+ button_change: Wijzig
+ button_change_password: Wijzig wachtwoord
+ button_check_all: Selecteer alle
+ button_clear: Leeg maken
+ button_configure: Configureer
+ button_copy: Kopiëer
+ button_create: Maak
+ button_delete: Verwijder
+ button_download: Download
+ button_edit: Bewerk
+ button_list: Lijst
+ button_lock: Sluit
+ button_log_time: Log tijd
+ button_login: Inloggen
+ button_move: Verplaatsen
+ button_quote: Citaat
+ button_rename: Hernoemen
+ button_reply: Antwoord
+ button_reset: Reset
+ button_rollback: Rollback naar deze versie
+ button_save: Bewaren
+ button_sort: Sorteer
+ button_submit: Toevoegen
+ button_test: Test
+ button_unarchive: Dearchiveer
+ button_uncheck_all: Deselecteer alle
+ button_unlock: Open
+ button_unwatch: Niet meer monitoren
+ button_update: Update
+ button_view: Bekijken
+ button_watch: Monitor
+ default_activity_design: Ontwerp
+ default_activity_development: Ontwikkeling
+ default_doc_category_tech: Technische documentatie
+ default_doc_category_user: Gebruikersdocumentatie
+ default_issue_status_in_progress: In Progress
+ default_issue_status_closed: Gesloten
+ default_issue_status_feedback: Terugkoppeling
+ default_issue_status_new: Nieuw
+ default_issue_status_rejected: Afgewezen
+ default_issue_status_resolved: Opgelost
+ default_priority_high: Hoog
+ default_priority_immediate: Onmiddellijk
+ default_priority_low: Laag
+ default_priority_normal: Normaal
+ default_priority_urgent: Spoed
+ default_role_developper: Ontwikkelaar
+ default_role_manager: Manager
+ default_role_reporter: Rapporteur
+ default_tracker_bug: Bug
+ default_tracker_feature: Feature
+ default_tracker_support: Support
+ enumeration_activities: Activiteiten (tijdtracking)
+ enumeration_doc_categories: Documentcategorieën
+ enumeration_issue_priorities: Issueprioriteiten
+ error_can_t_load_default_data: "De standaard configuratie kon niet worden geladen: {{value}}"
+ error_issue_not_found_in_project: 'Deze issue is niet gevonden of behoort niet toe tot dit project.'
+ error_scm_annotate: "Er kan geen commentaar toegevoegd worden."
+ error_scm_command_failed: "Er trad een fout op tijdens de poging om verbinding te maken met de repository: {{value}}"
+ error_scm_not_found: "Deze ingang of revisie bestaat niet in de repository."
+ field_account: Account
+ field_activity: Activiteit
+ field_admin: Beheerder
+ field_assignable: Issues kunnen toegewezen worden aan deze rol
+ field_assigned_to: Toegewezen aan
+ field_attr_firstname: Voornaam attribuut
+ field_attr_lastname: Achternaam attribuut
+ field_attr_login: Login attribuut
+ field_attr_mail: E-mail attribuut
+ field_auth_source: Authenticatiemethode
+ field_author: Auteur
+ field_base_dn: Base DN
+ field_category: Categorie
+ field_column_names: Kolommen
+ field_comments: Commentaar
+ field_comments_sorting: Commentaar weergeven
+ field_created_on: Aangemaakt
+ field_default_value: Standaardwaarde
+ field_delay: Vertraging
+ field_description: Beschrijving
+ field_done_ratio: % Gereed
+ field_downloads: Downloads
+ field_due_date: Verwachte datum gereed
+ field_effective_date: Datum
+ field_estimated_hours: Geschatte tijd
+ field_field_format: Formaat
+ field_filename: Bestand
+ field_filesize: Grootte
+ field_firstname: Voornaam
+ field_fixed_version: Versie
+ field_hide_mail: Verberg mijn e-mailadres
+ field_homepage: Homepage
+ field_host: Host
+ field_hours: Uren
+ field_identifier: Identificatiecode
+ field_is_closed: Issue gesloten
+ field_is_default: Standaard
+ field_is_filter: Gebruikt als een filter
+ field_is_for_all: Voor alle projecten
+ field_is_in_chlog: Issues weergegeven in wijzigingslog
+ field_is_in_roadmap: Issues weergegeven in roadmap
+ field_is_public: Publiek
+ field_is_required: Verplicht
+ field_issue: Issue
+ field_issue_to: Gerelateerd issue
+ field_language: Taal
+ field_last_login_on: Laatste bezoek
+ field_lastname: Achternaam
+ field_login: Inloggen
+ field_mail: E-mail
+ field_mail_notification: Mail mededelingen
+ field_max_length: Maximale lengte
+ field_min_length: Minimale lengte
+ field_name: Naam
+ field_new_password: Nieuw wachtwoord
+ field_notes: Notities
+ field_onthefly: On-the-fly aanmaken van een gebruiker
+ field_parent: Subproject van
+ field_parent_title: Bovenliggende pagina
+ field_password: Wachtwoord
+ field_password_confirmation: Bevestigen
+ field_port: Port
+ field_possible_values: Mogelijke waarden
+ field_priority: Prioriteit
+ field_project: Project
+ field_redirect_existing_links: Verwijs bestaande links door
+ field_regexp: Reguliere expressie
+ field_role: Rol
+ field_searchable: Doorzoekbaar
+ field_spent_on: Datum
+ field_start_date: Startdatum
+ field_start_page: Startpagina
+ field_status: Status
+ field_subject: Onderwerp
+ field_subproject: Subproject
+ field_summary: Samenvatting
+ field_time_zone: Tijdzone
+ field_title: Titel
+ field_tracker: Tracker
+ field_type: Type
+ field_updated_on: Gewijzigd
+ field_url: URL
+ field_user: Gebruiker
+ field_value: Waarde
+ field_version: Versie
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_csv_separator: ','
+ general_first_day_of_week: '7'
+ general_lang_name: 'Nederlands'
+ general_pdf_encoding: ISO-8859-1
+ general_text_No: 'Nee'
+ general_text_Yes: 'Ja'
+ general_text_no: 'nee'
+ general_text_yes: 'ja'
+ gui_validation_error: 1 fout
+ gui_validation_error_plural: "{{count}} fouten"
+ label_activity: Activiteit
+ label_add_another_file: Ander bestand toevoegen
+ label_add_note: Voeg een notitie toe
+ label_added: toegevoegd
+ label_added_time_by: "Toegevoegd door {{author}} {{age}} geleden"
+ label_administration: Administratie
+ label_age: Leeftijd
+ label_ago: dagen geleden
+ label_all: alle
+ label_all_time: alles
+ label_all_words: Alle woorden
+ label_and_its_subprojects: "{{value}} en zijn subprojecten."
+ label_applied_status: Toegekende status
+ label_assigned_to_me_issues: Aan mij toegewezen issues
+ label_associated_revisions: Geassociëerde revisies
+ label_attachment: Bestand
+ label_attachment_delete: Verwijder bestand
+ label_attachment_new: Nieuw bestand
+ label_attachment_plural: Bestanden
+ label_attribute: Attribuut
+ label_attribute_plural: Attributen
+ label_auth_source: Authenticatiemodus
+ label_auth_source_new: Nieuwe authenticatiemodus
+ label_auth_source_plural: Authenticatiemodi
+ label_authentication: Authenticatie
+ label_blocked_by: geblokkeerd door
+ label_blocks: blokkeert
+ label_board: Forum
+ label_board_new: Nieuw forum
+ label_board_plural: Forums
+ label_boolean: Boolean
+ label_browse: Blader
+ label_bulk_edit_selected_issues: Bewerk geselecteerde issues in bulk
+ label_calendar: Kalender
+ label_change_log: Wijzigingslog
+ label_change_plural: Wijzigingen
+ label_change_properties: Eigenschappen wijzigen
+ label_change_status: Wijzig status
+ label_change_view_all: Bekijk alle wijzigingen
+ label_changes_details: Details van alle wijzigingen
+ label_changeset_plural: Changesets
+ label_chronological_order: In chronologische volgorde
+ label_closed_issues: gesloten
+ label_closed_issues_plural: gesloten
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_comment: Commentaar
+ label_comment_add: Voeg commentaar toe
+ label_comment_added: Commentaar toegevoegd
+ label_comment_delete: Verwijder commentaar
+ label_comment_plural: Commentaar
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_commits_per_author: Commits per auteur
+ label_commits_per_month: Commits per maand
+ label_confirmation: Bevestiging
+ label_contains: bevat
+ label_copied: gekopieerd
+ label_copy_workflow_from: Kopieer workflow van
+ label_current_status: Huidige status
+ label_current_version: Huidige versie
+ label_custom_field: Specifiek veld
+ label_custom_field_new: Nieuw specifiek veld
+ label_custom_field_plural: Specifieke velden
+ label_date: Datum
+ label_date_from: Van
+ label_date_range: Datumbereik
+ label_date_to: Tot
+ label_day_plural: dagen
+ label_default: Standaard
+ label_default_columns: Standaard kolommen.
+ label_deleted: verwijderd
+ label_details: Details
+ label_diff_inline: inline
+ label_diff_side_by_side: naast elkaar
+ label_disabled: uitgeschakeld
+ label_display_per_page: "Per pagina: {{value}}"
+ label_document: Document
+ label_document_added: Document toegevoegd
+ label_document_new: Nieuw document
+ label_document_plural: Documenten
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_downloads_abbr: D/L
+ label_duplicated_by: gedupliceerd door
+ label_duplicates: dupliceert
+ label_end_to_end: eind tot eind
+ label_end_to_start: eind tot start
+ label_enumeration_new: Nieuwe waarde
+ label_enumerations: Enumeraties
+ label_environment: Omgeving
+ label_equals: is gelijk
+ label_example: Voorbeeld
+ label_export_to: Exporteer naar
+ label_f_hour: "{{value}} uur"
+ label_f_hour_plural: "{{value}} uren"
+ label_feed_plural: Feeds
+ label_feeds_access_key_created_on: "RSS toegangssleutel {{value}} geleden gemaakt."
+ label_file_added: Bericht toegevoegd
+ label_file_plural: Bestanden
+ label_filter_add: Voeg filter toe
+ label_filter_plural: Filters
+ label_float: Float
+ label_follows: volgt op
+ label_gantt: Gantt
+ label_general: Algemeen
+ label_generate_key: Genereer een sleutel
+ label_help: Help
+ label_history: Geschiedenis
+ label_home: Home
+ label_in: in
+ label_in_less_than: in minder dan
+ label_in_more_than: in meer dan
+ label_incoming_emails: Inkomende e-mail
+ label_index_by_date: Indexeer op datum
+ label_index_by_title: Indexeer op titel
+ label_information: Informatie
+ label_information_plural: Informatie
+ label_integer: Integer
+ label_internal: Intern
+ label_issue: Issue
+ label_issue_added: Issue toegevoegd
+ label_issue_category: Issuecategorie
+ label_issue_category_new: Nieuwe categorie
+ label_issue_category_plural: Issuecategorieën
+ label_issue_new: Nieuw issue
+ label_issue_plural: Issues
+ label_issue_status: Issuestatus
+ label_issue_status_new: Nieuwe status
+ label_issue_status_plural: Issue statussen
+ label_issue_tracking: Issue-tracking
+ label_issue_updated: Issue bijgewerkt
+ label_issue_view_all: Bekijk alle issues
+ label_issue_watchers: Monitoren
+ label_issues_by: "Issues door {{value}}"
+ label_jump_to_a_project: Ga naar een project...
+ label_language_based: Taal gebaseerd
+ label_last_changes: "laatste {{count}} wijzigingen"
+ label_last_login: Laatste bezoek
+ label_last_month: laatste maand
+ label_last_n_days: "{{count}} dagen geleden"
+ label_last_week: vorige week
+ label_latest_revision: Meest recente revisie
+ label_latest_revision_plural: Meest recente revisies
+ label_ldap_authentication: LDAP authenticatie
+ label_less_than_ago: minder dan x dagen geleden
+ label_list: Lijst
+ label_loading: Laden...
+ label_logged_as: Ingelogd als
+ label_login: Inloggen
+ label_logout: Uitloggen
+ label_max_size: Maximumgrootte
+ label_me: mij
+ label_member: Lid
+ label_member_new: Nieuw lid
+ label_member_plural: Leden
+ label_message_last: Laatste bericht
+ label_message_new: Nieuw bericht
+ label_message_plural: Berichten
+ label_message_posted: Bericht toegevoegd
+ label_min_max_length: Min-max lengte
+ label_modification: "{{count}} wijziging"
+ label_modification_plural: "{{count}} wijzigingen"
+ label_modified: gewijzigd
+ label_module_plural: Modules
+ label_month: Maand
+ label_months_from: maanden vanaf
+ label_more: Meer
+ label_more_than_ago: meer dan x dagen geleden
+ label_my_account: Mijn account
+ label_my_page: Mijn pagina
+ label_my_projects: Mijn projecten
+ label_new: Nieuw
+ label_new_statuses_allowed: Nieuwe toegestane statussen
+ label_news: Nieuws
+ label_news_added: Nieuws toegevoegd
+ label_news_latest: Laatste nieuws
+ label_news_new: Voeg nieuws toe
+ label_news_plural: Nieuws
+ label_news_view_all: Bekijk al het nieuws
+ label_next: Volgende
+ label_no_change_option: (Geen wijziging)
+ label_no_data: Geen gegevens om te tonen
+ label_nobody: niemand
+ label_none: geen
+ label_not_contains: bevat niet
+ label_not_equals: is niet gelijk
+ label_open_issues: open
+ label_open_issues_plural: open
+ label_optional_description: Optionele beschrijving
+ label_options: Opties
+ label_overall_activity: Activiteit
+ label_overview: Overzicht
+ label_password_lost: Wachtwoord verloren
+ label_per_page: Per pagina
+ label_permissions: Permissies
+ label_permissions_report: Permissierapport
+ label_personalize_page: Personaliseer deze pagina
+ label_planning: Planning
+ label_please_login: Log a.u.b. in
+ label_plugins: Plugins
+ label_precedes: gaat vooraf aan
+ label_preferences: Voorkeuren
+ label_preview: Voorbeeldweergave
+ label_previous: Vorige
+ label_project: Project
+ label_project_all: Alle projecten
+ label_project_latest: Nieuwste projecten
+ label_project_new: Nieuw project
+ label_project_plural: Projecten
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_public_projects: Publieke projecten
+ label_query: Eigen zoekvraag
+ label_query_new: Nieuwe zoekvraag
+ label_query_plural: Eigen zoekvragen
+ label_read: Lees...
+ label_register: Registreer
+ label_registered_on: Geregistreerd op
+ label_registration_activation_by_email: accountactivatie per e-mail
+ label_registration_automatic_activation: automatische accountactivatie
+ label_registration_manual_activation: handmatige accountactivatie
+ label_related_issues: Gerelateerde issues
+ label_relates_to: gerelateerd aan
+ label_relation_delete: Verwijder relatie
+ label_relation_new: Nieuwe relatie
+ label_renamed: hernoemd
+ label_reply_plural: Antwoorden
+ label_report: Rapport
+ label_report_plural: Rapporten
+ label_reported_issues: Gemelde issues
+ label_repository: Repository
+ label_repository_plural: Repositories
+ label_result_plural: Resultaten
+ label_reverse_chronological_order: In omgekeerde chronologische volgorde
+ label_revision: Revisie
+ label_revision_plural: Revisies
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Voldaan in {{value}}"
+ label_roadmap_no_issues: Geen issues voor deze versie
+ label_roadmap_overdue: "{{value}} over tijd"
+ label_role: Rol
+ label_role_and_permissions: Rollen en permissies
+ label_role_new: Nieuwe rol
+ label_role_plural: Rollen
+ label_scm: SCM
+ label_search: Zoeken
+ label_search_titles_only: Enkel titels doorzoeken
+ label_send_information: Stuur accountinformatie naar de gebruiker
+ label_send_test_email: Stuur een test e-mail
+ label_settings: Instellingen
+ label_show_completed_versions: Toon afgeronde versies
+ label_sort_by: "Sorteer op {{value}}"
+ label_sort_higher: Verplaats naar boven
+ label_sort_highest: Verplaats naar begin
+ label_sort_lower: Verplaats naar beneden
+ label_sort_lowest: Verplaats naar eind
+ label_spent_time: Gespendeerde tijd
+ label_start_to_end: start tot eind
+ label_start_to_start: start tot start
+ label_statistics: Statistieken
+ label_stay_logged_in: Blijf ingelogd
+ label_string: Tekst
+ label_subproject_plural: Subprojecten
+ label_text: Lange tekst
+ label_theme: Thema
+ label_this_month: deze maand
+ label_this_week: deze week
+ label_this_year: dit jaar
+ label_time_tracking: Tijdregistratie bijhouden
+ label_today: vandaag
+ label_topic_plural: Onderwerpen
+ label_total: Totaal
+ label_tracker: Tracker
+ label_tracker_new: Nieuwe tracker
+ label_tracker_plural: Trackers
+ label_updated_time: "{{value}} geleden bijgewerkt"
+ label_updated_time_by: "{{age}} geleden bijgewerkt door {{author}}"
+ label_used_by: Gebruikt door
+ label_user: Gebruiker
+ label_user_activity: "{{value}}'s activiteit"
+ label_user_mail_no_self_notified: "Ik wil niet verwittigd worden van wijzigingen die ik zelf maak."
+ label_user_mail_option_all: "Bij elk gebeurtenis in al mijn projecten..."
+ label_user_mail_option_none: "Alleen in de dingen die ik monitor of in betrokken ben"
+ label_user_mail_option_selected: "Enkel bij elke gebeurtenis op het geselecteerde project..."
+ label_user_new: Nieuwe gebruiker
+ label_user_plural: Gebruikers
+ label_version: Versie
+ label_version_new: Nieuwe versie
+ label_version_plural: Versies
+ label_view_diff: Bekijk verschillen
+ label_view_revisions: Bekijk revisies
+ label_watched_issues: Gemonitorde issues
+ label_week: Week
+ label_wiki: Wiki
+ label_wiki_edit: Wiki edit
+ label_wiki_edit_plural: Wiki edits
+ label_wiki_page: Wikipagina
+ label_wiki_page_plural: Wikipagina's
+ label_workflow: Workflow
+ label_year: Jaar
+ label_yesterday: gisteren
+ mail_body_account_activation_request: "Een nieuwe gebruiker ({{value}}) is geregistreerd. Zijn account wacht op uw akkoord:"
+ mail_body_account_information: Uw account gegevens
+ mail_body_account_information_external: "U kunt uw account ({{value}}) gebruiken om in te loggen."
+ mail_body_lost_password: 'Gebruik de volgende link om uw wachtwoord te wijzigen:'
+ mail_body_register: 'Gebruik de volgende link om uw account te activeren:'
+ mail_body_reminder: "{{count}} issue(s) die aan u toegewezen zijn en voldaan moeten zijn in de komende {{days}} dagen:"
+ mail_subject_account_activation_request: "{{value}} accountactivatieverzoek"
+ mail_subject_lost_password: "uw {{value}} wachtwoord"
+ mail_subject_register: "uw {{value}} accountactivatie"
+ mail_subject_reminder: "{{count}} issue(s) die voldaan moeten zijn in de komende dagen."
+ notice_account_activated: uw account is geactiveerd. u kunt nu inloggen.
+ notice_account_invalid_creditentials: Incorrecte gebruikersnaam of wachtwoord
+ notice_account_lost_email_sent: Er is een e-mail naar u verstuurd met instructies over het kiezen van een nieuw wachtwoord.
+ notice_account_password_updated: Wachtwoord is met succes gewijzigd
+ notice_account_pending: "Uw account is aangemaakt, maar wacht nog op goedkeuring van de beheerder."
+ notice_account_register_done: Account is met succes aangemaakt.
+ notice_account_unknown_email: Onbekende gebruiker.
+ notice_account_updated: Account is met succes gewijzigd
+ notice_account_wrong_password: Incorrect wachtwoord
+ notice_can_t_change_password: Dit account gebruikt een externe bron voor authenticatie. Het is niet mogelijk om het wachtwoord te veranderen.
+ notice_default_data_loaded: Standaard configuratie succesvol geladen.
+ notice_email_error: "Er is een fout opgetreden tijdens het versturen van ({{value}})"
+ notice_email_sent: "Een e-mail werd verstuurd naar {{value}}"
+ notice_failed_to_save_issues: "Fout bij bewaren van {{count}} issue(s) ({{total}} geselecteerd): {{ids}}."
+ notice_feeds_access_key_reseted: Je RSS toegangssleutel werd gereset.
+ notice_file_not_found: De pagina die u probeerde te benaderen bestaat niet of is verwijderd.
+ notice_locking_conflict: De gegevens zijn gewijzigd door een andere gebruiker.
+ notice_no_issue_selected: "Er is geen issue geselecteerd. Selecteer de issue die u wilt bewerken."
+ notice_not_authorized: Het is u niet toegestaan deze pagina te raadplegen.
+ notice_successful_connection: Verbinding succesvol.
+ notice_successful_create: Succesvol aangemaakt.
+ notice_successful_delete: Succesvol verwijderd.
+ notice_successful_update: Wijzigen succesvol.
+ notice_unable_delete_version: Niet mogelijk om deze versie te verwijderen.
+ permission_add_issue_notes: Voeg notities toe
+ permission_add_issue_watchers: Voeg monitors toe
+ permission_add_issues: Voeg issues toe
+ permission_add_messages: Voeg berichten toe
+ permission_browse_repository: Repository doorbladeren
+ permission_comment_news: Nieuws commentaar geven
+ permission_commit_access: Commit toegang
+ permission_delete_issues: Issues verwijderen
+ permission_delete_messages: Berichten verwijderen
+ permission_delete_own_messages: Eigen berichten verwijderen
+ permission_delete_wiki_pages: Wiki pagina's verwijderen
+ permission_delete_wiki_pages_attachments: Bijlagen verwijderen
+ permission_edit_issue_notes: Notities bewerken
+ permission_edit_issues: Issues bewerken
+ permission_edit_messages: Berichten bewerken
+ permission_edit_own_issue_notes: Eigen notities bewerken
+ permission_edit_own_messages: Eigen berichten bewerken
+ permission_edit_own_time_entries: Eigen tijdlogboek bewerken
+ permission_edit_project: Project bewerken
+ permission_edit_time_entries: Tijdlogboek bewerken
+ permission_edit_wiki_pages: Wiki pagina's bewerken
+ permission_log_time: Gespendeerde tijd loggen
+ permission_manage_boards: Forums beheren
+ permission_manage_categories: Issue-categorieën beheren
+ permission_manage_documents: Documenten beheren
+ permission_manage_files: Bestanden beheren
+ permission_manage_issue_relations: Issuerelaties beheren
+ permission_manage_members: Leden beheren
+ permission_manage_news: Nieuws beheren
+ permission_manage_public_queries: Publieke queries beheren
+ permission_manage_repository: Repository beheren
+ permission_manage_versions: Versiebeheer
+ permission_manage_wiki: Wikibeheer
+ permission_move_issues: Issues verplaatsen
+ permission_protect_wiki_pages: Wikipagina's beschermen
+ permission_rename_wiki_pages: Wikipagina's hernoemen
+ permission_save_queries: Queries opslaan
+ permission_select_project_modules: Project modules selecteren
+ permission_view_calendar: Kalender bekijken
+ permission_view_changesets: Changesets bekijken
+ permission_view_documents: Documenten bekijken
+ permission_view_files: Bestanden bekijken
+ permission_view_gantt: Gantt grafiek bekijken
+ permission_view_issue_watchers: Monitorlijst bekijken
+ permission_view_messages: Berichten bekijken
+ permission_view_time_entries: Gespendeerde tijd bekijken
+ permission_view_wiki_edits: Wikihistorie bekijken
+ permission_view_wiki_pages: Wikipagina's bekijken
+ project_module_boards: Forums
+ project_module_documents: Documenten
+ project_module_files: Bestanden
+ project_module_issue_tracking: Issue tracking
+ project_module_news: Nieuws
+ project_module_repository: Repository
+ project_module_time_tracking: Tijd tracking
+ project_module_wiki: Wiki
+ setting_activity_days_default: Aantal dagen getoond bij het tabblad "Activiteit"
+ setting_app_subtitle: Applicatieondertitel
+ setting_app_title: Applicatietitel
+ setting_attachment_max_size: Attachment max. grootte
+ setting_autofetch_changesets: Haal commits automatisch op
+ setting_autologin: Automatisch inloggen
+ setting_bcc_recipients: Blind carbon copy ontvangers (bcc)
+ setting_commit_fix_keywords: Gefixeerde trefwoorden
+ setting_commit_logs_encoding: Encodering van commit berichten
+ setting_commit_ref_keywords: Refererende trefwoorden
+ setting_cross_project_issue_relations: Sta crossproject issuerelaties toe
+ setting_date_format: Datumformaat
+ setting_default_language: Standaard taal
+ setting_default_projects_public: Nieuwe projecten zijn standaard publiek
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ setting_display_subprojects_issues: Standaard issues van subproject tonen
+ setting_emails_footer: E-mails footer
+ setting_enabled_scm: SCM ingeschakeld
+ setting_feeds_limit: Feedinhoudlimiet
+ setting_gravatar_enabled: Gebruik Gravatar gebruikersiconen
+ setting_host_name: Hostnaam
+ setting_issue_list_default_columns: Standaardkolommen getoond op de lijst met issues
+ setting_issues_export_limit: Limiet export issues
+ setting_login_required: Authenticatie vereist
+ setting_mail_from: Afzender e-mail adres
+ setting_mail_handler_api_enabled: Schakel WS in voor inkomende mail.
+ setting_mail_handler_api_key: API sleutel
+ setting_per_page_options: Objects per pagina-opties
+ setting_plain_text_mail: platte tekst (geen HTML)
+ setting_protocol: Protocol
+ setting_repositories_encodings: Repositories coderingen
+ setting_self_registration: Zelfregistratie toegestaan
+ setting_sequential_project_identifiers: Genereer sequentiële projectidentiteiten
+ setting_sys_api_enabled: Gebruik WS voor repository beheer
+ setting_text_formatting: Tekstformaat
+ setting_time_format: Tijd formaat
+ setting_user_format: Gebruikers weergaveformaat
+ setting_welcome_text: Welkomsttekst
+ setting_wiki_compression: Wikigeschiedenis comprimeren
+ status_active: actief
+ status_locked: gelockt
+ status_registered: geregistreerd
+ text_are_you_sure: Weet u het zeker?
+ text_assign_time_entries_to_project: Gerapporteerde uren toevoegen aan dit project
+ text_caracters_maximum: "{{count}} van maximum aantal tekens."
+ text_caracters_minimum: "Moet minstens {{count}} karakters lang zijn."
+ text_comma_separated: Meerdere waarden toegestaan (kommagescheiden).
+ text_default_administrator_account_changed: Standaard beheerderaccount gewijzigd
+ text_destroy_time_entries: Verwijder gerapporteerde uren
+ text_destroy_time_entries_question: "{{hours}} uren werden gerapporteerd op de issue(s) die u wilde verwijderen. Wat wil u doen?"
+ text_diff_truncated: '... Deze diff werd afgekort omdat het de maximale weer te geven karakters overschreed.'
+ text_email_delivery_not_configured: "E-mailbezorging is niet geconfigureerd. Notificaties zijn uitgeschakeld.\nConfigureer uw SMTP server in config/email.yml en herstart de applicatie om dit te activeren."
+ text_enumeration_category_reassign_to: 'Wijs de volgende waarde toe:'
+ text_enumeration_destroy_question: "{{count}} objecten zijn toegewezen aan deze waarde."
+ text_file_repository_writable: Bestandsrepository beschrijfbaar
+ text_issue_added: "Issue {{id}} is gerapporteerd (door {{author}})."
+ text_issue_category_destroy_assignments: Verwijder toewijzingen aan deze categorie
+ text_issue_category_destroy_question: "Er zijn issues ({{count}}) aan deze categorie toegewezen. Wat wilt u hiermee doen ?"
+ text_issue_category_reassign_to: Issues opnieuw toewijzen aan deze categorie
+ text_issue_updated: "Issue {{id}} is gewijzigd (door {{author}})."
+ text_issues_destroy_confirmation: 'Weet u zeker dat u deze issue(s) wil verwijderen?'
+ text_issues_ref_in_commit_messages: Opzoeken en aanpassen van issues in commitberichten
+ text_length_between: "Lengte tussen {{min}} en {{max}} tekens."
+ text_load_default_configuration: Laad de standaardconfiguratie
+ text_min_max_length_info: 0 betekent geen restrictie
+ text_no_configuration_data: "Rollen, trackers, issue statussen en workflows zijn nog niet geconfigureerd.\nHet is ten zeerste aangeraden om de standaard configuratie in te laden. U kunt deze aanpassen nadat deze is ingeladen."
+ text_plugin_assets_writable: Plugin assets directory writable
+ text_project_destroy_confirmation: Weet u zeker dat u dit project en alle gerelateerde gegevens wilt verwijderen?
+ text_project_identifier_info: 'kleine letters (a-z), cijfers en liggende streepjes toegestaan.<br />Eenmaal bewaard kan de identificatiecode niet meer worden gewijzigd.'
+ text_reassign_time_entries: 'Gerapporteerde uren opnieuw toewijzen:'
+ text_regexp_info: bv. ^[A-Z0-9]+$
+ text_repository_usernames_mapping: "Koppel de Redminegebruikers aan gebruikers in de repository log.\nGebruikers met dezelfde Redmine en repository gebruikersnaam of email worden automatisch gekoppeld."
+ text_rmagick_available: RMagick beschikbaar (optioneel)
+ text_select_mail_notifications: Selecteer acties waarvoor mededelingen via mail moeten worden verstuurd.
+ text_select_project_modules: 'Selecteer de modules die u wilt gebruiken voor dit project:'
+ text_status_changed_by_changeset: "Toegepast in changeset {{value}}."
+ text_subprojects_destroy_warning: "De subprojecten: {{value}} zullen ook verwijderd worden."
+ text_tip_task_begin_day: taak die op deze dag begint
+ text_tip_task_begin_end_day: taak die op deze dag begint en eindigt
+ text_tip_task_end_day: taak die op deze dag eindigt
+ text_tracker_no_workflow: Geen workflow gedefinieerd voor deze tracker
+ text_unallowed_characters: Niet toegestane tekens
+ text_user_mail_option: "Bij niet-geselecteerde projecten zult u enkel notificaties ontvangen voor issues die u monitort of waar u bij betrokken bent (als auteur of toegewezen persoon)."
+ text_user_wrote: "{{value}} schreef:"
+ text_wiki_destroy_confirmation: Weet u zeker dat u deze wiki en zijn inhoud wenst te verwijderen?
+ text_workflow_edit: Selecteer een rol en een tracker om de workflow te wijzigen
+ warning_attachments_not_saved: "{{count}} bestand(en) konden niet opgeslagen worden."
+ button_create_and_continue: Maak en ga verder
+ text_custom_field_possible_values_info: 'Per lijn een waarde'
+ label_display: Toon
+ field_editable: Bewerkbaar
+ setting_repository_log_display_limit: Maximum hoeveelheid van revisies zichbaar
+ setting_file_max_size_displayed: Max grootte van tekst bestanden inline zichtbaar
+ field_watcher: Watcher
+ setting_openid: Sta OpenID login en registratie toe
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: of login met je OpenID
+ field_content: Content
+ label_descending: Aflopend
+ label_sort: Sorteer
+ label_ascending: Oplopend
+ label_date_from_to: Van {{start}} tot {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Deze pagina heeft {{descendants}} subpagina's en onderliggende pagina's?. Wil wil je ermee doen?
+ text_wiki_page_reassign_children: Alle subpagina's toewijzen aan deze hoofdpagina
+ text_wiki_page_nullify_children: Behoud subpagina's als hoofdpagina's
+ text_wiki_page_destroy_children: Verwijder alle subpagina's en onderliggende pagina's
+ setting_password_min_length: Minimum wachtwoord lengte
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Norwegian, norsk bokmål, by irb.no
+"no":
+ support:
+ array:
+ sentence_connector: "og"
+ date:
+ formats:
+ default: "%d.%m.%Y"
+ short: "%e. %b"
+ long: "%e. %B %Y"
+ day_names: [søndag, mandag, tirsdag, onsdag, torsdag, fredag, lørdag]
+ abbr_day_names: [søn, man, tir, ons, tor, fre, lør]
+ month_names: [~, januar, februar, mars, april, mai, juni, juli, august, september, oktober, november, desember]
+ abbr_month_names: [~, jan, feb, mar, apr, mai, jun, jul, aug, sep, okt, nov, des]
+ order: [:day, :month, :year]
+ time:
+ formats:
+ default: "%A, %e. %B %Y, %H:%M"
+ time: "%H:%M"
+ short: "%e. %B, %H:%M"
+ long: "%A, %e. %B %Y, %H:%M"
+ am: ""
+ pm: ""
+ datetime:
+ distance_in_words:
+ half_a_minute: "et halvt minutt"
+ less_than_x_seconds:
+ one: "mindre enn 1 sekund"
+ other: "mindre enn {{count}} sekunder"
+ x_seconds:
+ one: "1 sekund"
+ other: "{{count}} sekunder"
+ less_than_x_minutes:
+ one: "mindre enn 1 minutt"
+ other: "mindre enn {{count}} minutter"
+ x_minutes:
+ one: "1 minutt"
+ other: "{{count}} minutter"
+ about_x_hours:
+ one: "rundt 1 time"
+ other: "rundt {{count}} timer"
+ x_days:
+ one: "1 dag"
+ other: "{{count}} dager"
+ about_x_months:
+ one: "rundt 1 måned"
+ other: "rundt {{count}} måneder"
+ x_months:
+ one: "1 måned"
+ other: "{{count}} måneder"
+ about_x_years:
+ one: "rundt 1 år"
+ other: "rundt {{count}} år"
+ over_x_years:
+ one: "over 1 år"
+ other: "over {{count}} år"
+ number:
+ format:
+ precision: 2
+ separator: "."
+ delimiter: ","
+ currency:
+ format:
+ unit: "kr"
+ format: "%n %u"
+ precision:
+ format:
+ delimiter: ""
+ precision: 4
+ human:
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ activerecord:
+ errors:
+ template:
+ header: "kunne ikke lagre {{model}} på grunn av {{count}} feil."
+ body: "det oppstod problemer i følgende felt:"
+ messages:
+ inclusion: "er ikke inkludert i listen"
+ exclusion: "er reservert"
+ invalid: "er ugyldig"
+ confirmation: "passer ikke bekreftelsen"
+ accepted: "må være akseptert"
+ empty: "kan ikke være tom"
+ blank: "kan ikke være blank"
+ too_long: "er for lang (maksimum {{count}} tegn)"
+ too_short: "er for kort (minimum {{count}} tegn)"
+ wrong_length: "er av feil lengde (maksimum {{count}} tegn)"
+ taken: "er allerede i bruk"
+ not_a_number: "er ikke et tall"
+ greater_than: "må være større enn {{count}}"
+ greater_than_or_equal_to: "må være større enn eller lik {{count}}"
+ equal_to: "må være lik {{count}}"
+ less_than: "må være mindre enn {{count}}"
+ less_than_or_equal_to: "må være mindre enn eller lik {{count}}"
+ odd: "må være oddetall"
+ even: "må være partall"
+ greater_than_start_date: "må være større enn startdato"
+ not_same_project: "hører ikke til samme prosjekt"
+ circular_dependency: "Denne relasjonen ville lagd en sirkulær avhengighet"
+
+
+ actionview_instancetag_blank_option: Vennligst velg
+
+ general_text_No: 'Nei'
+ general_text_Yes: 'Ja'
+ general_text_no: 'nei'
+ general_text_yes: 'ja'
+ general_lang_name: 'Norwegian (Norsk bokmål)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Kontoen er oppdatert.
+ notice_account_invalid_creditentials: Feil brukernavn eller passord
+ notice_account_password_updated: Passordet er oppdatert.
+ notice_account_wrong_password: Feil passord
+ notice_account_register_done: Kontoen er opprettet. Klikk lenken som er sendt deg i e-post for å aktivere kontoen.
+ notice_account_unknown_email: Ukjent bruker.
+ notice_can_t_change_password: Denne kontoen bruker ekstern godkjenning. Passordet kan ikke endres.
+ notice_account_lost_email_sent: En e-post med instruksjoner for å velge et nytt passord er sendt til deg.
+ notice_account_activated: Din konto er aktivert. Du kan nå logge inn.
+ notice_successful_create: Opprettet.
+ notice_successful_update: Oppdatert.
+ notice_successful_delete: Slettet.
+ notice_successful_connection: Koblet opp.
+ notice_file_not_found: Siden du forsøkte å vise eksisterer ikke, eller er slettet.
+ notice_locking_conflict: Data har blitt oppdatert av en annen bruker.
+ notice_not_authorized: Du har ikke adgang til denne siden.
+ notice_email_sent: "En e-post er sendt til {{value}}"
+ notice_email_error: "En feil oppstod under sending av e-post ({{value}})"
+ notice_feeds_access_key_reseted: Din RSS-tilgangsnøkkel er nullstilt.
+ notice_failed_to_save_issues: "Lykkes ikke å lagre {{count}} sak(er) på {{total}} valgt: {{ids}}."
+ notice_no_issue_selected: "Ingen sak valgt! Vennligst merk sakene du vil endre."
+ notice_account_pending: "Din konto ble opprettet og avventer nå administrativ godkjenning."
+ notice_default_data_loaded: Standardkonfigurasjonen lastet inn.
+
+ error_can_t_load_default_data: "Standardkonfigurasjonen kunne ikke lastes inn: {{value}}"
+ error_scm_not_found: "Elementet og/eller revisjonen eksisterer ikke i depoet."
+ error_scm_command_failed: "En feil oppstod under tilkobling til depoet: {{value}}"
+ error_scm_annotate: "Elementet eksisterer ikke, eller kan ikke noteres."
+ error_issue_not_found_in_project: 'Saken eksisterer ikke, eller hører ikke til dette prosjektet'
+
+ mail_subject_lost_password: "Ditt {{value}} passord"
+ mail_body_lost_password: 'Klikk følgende lenke for å endre ditt passord:'
+ mail_subject_register: "{{value}} kontoaktivering"
+ mail_body_register: 'Klikk følgende lenke for å aktivere din konto:'
+ mail_body_account_information_external: "Du kan bruke din {{value}}-konto for å logge inn."
+ mail_body_account_information: Informasjon om din konto
+ mail_subject_account_activation_request: "{{value}} kontoaktivering"
+ mail_body_account_activation_request: "En ny bruker ({{value}}) er registrert, og avventer din godkjenning:"
+ mail_subject_reminder: "{{count}} sak(er) har frist de kommende dagene"
+ mail_body_reminder: "{{count}} sak(er) som er tildelt deg har frist de kommende {{days}} dager:"
+
+ gui_validation_error: 1 feil
+ gui_validation_error_plural: "{{count}} feil"
+
+ field_name: Navn
+ field_description: Beskrivelse
+ field_summary: Oppsummering
+ field_is_required: Kreves
+ field_firstname: Fornavn
+ field_lastname: Etternavn
+ field_mail: E-post
+ field_filename: Fil
+ field_filesize: Størrelse
+ field_downloads: Nedlastinger
+ field_author: Forfatter
+ field_created_on: Opprettet
+ field_updated_on: Oppdatert
+ field_field_format: Format
+ field_is_for_all: For alle prosjekter
+ field_possible_values: Lovlige verdier
+ field_regexp: Regular expression
+ field_min_length: Minimum lengde
+ field_max_length: Maksimum lengde
+ field_value: Verdi
+ field_category: Kategori
+ field_title: Tittel
+ field_project: Prosjekt
+ field_issue: Sak
+ field_status: Status
+ field_notes: Notater
+ field_is_closed: Lukker saken
+ field_is_default: Standardverdi
+ field_tracker: Sakstype
+ field_subject: Emne
+ field_due_date: Frist
+ field_assigned_to: Tildelt til
+ field_priority: Prioritet
+ field_fixed_version: Mål-versjon
+ field_user: Bruker
+ field_role: Rolle
+ field_homepage: Hjemmeside
+ field_is_public: Offentlig
+ field_parent: Underprosjekt til
+ field_is_in_chlog: Vises i endringslogg
+ field_is_in_roadmap: Vises i veikart
+ field_login: Brukernavn
+ field_mail_notification: E-post-varsling
+ field_admin: Administrator
+ field_last_login_on: Sist innlogget
+ field_language: Språk
+ field_effective_date: Dato
+ field_password: Passord
+ field_new_password: Nytt passord
+ field_password_confirmation: Bekreft passord
+ field_version: Versjon
+ field_type: Type
+ field_host: Vert
+ field_port: Port
+ field_account: Konto
+ field_base_dn: Base DN
+ field_attr_login: Brukernavnsattributt
+ field_attr_firstname: Fornavnsattributt
+ field_attr_lastname: Etternavnsattributt
+ field_attr_mail: E-post-attributt
+ field_onthefly: On-the-fly brukeropprettelse
+ field_start_date: Start
+ field_done_ratio: % Ferdig
+ field_auth_source: Autentifikasjonsmodus
+ field_hide_mail: Skjul min e-post-adresse
+ field_comments: Kommentarer
+ field_url: URL
+ field_start_page: Startside
+ field_subproject: Underprosjekt
+ field_hours: Timer
+ field_activity: Aktivitet
+ field_spent_on: Dato
+ field_identifier: Identifikasjon
+ field_is_filter: Brukes som filter
+ field_issue_to: Relatert saker
+ field_delay: Forsinkelse
+ field_assignable: Saker kan tildeles denne rollen
+ field_redirect_existing_links: Viderekoble eksisterende lenker
+ field_estimated_hours: Estimert tid
+ field_column_names: Kolonner
+ field_time_zone: Tidssone
+ field_searchable: Søkbar
+ field_default_value: Standardverdi
+ field_comments_sorting: Vis kommentarer
+
+ setting_app_title: Applikasjonstittel
+ setting_app_subtitle: Applikasjonens undertittel
+ setting_welcome_text: Velkomsttekst
+ setting_default_language: Standardspråk
+ setting_login_required: Krever innlogging
+ setting_self_registration: Selvregistrering
+ setting_attachment_max_size: Maks. størrelse vedlegg
+ setting_issues_export_limit: Eksportgrense for saker
+ setting_mail_from: Avsenders e-post
+ setting_bcc_recipients: Blindkopi (bcc) til mottakere
+ setting_host_name: Vertsnavn
+ setting_text_formatting: Tekstformattering
+ setting_wiki_compression: Komprimering av Wiki-historikk
+ setting_feeds_limit: Innholdsgrense for Feed
+ setting_default_projects_public: Nye prosjekter er offentlige som standard
+ setting_autofetch_changesets: Autohenting av innsendinger
+ setting_sys_api_enabled: Aktiver webservice for depot-administrasjon
+ setting_commit_ref_keywords: Nøkkelord for referanse
+ setting_commit_fix_keywords: Nøkkelord for retting
+ setting_autologin: Autoinnlogging
+ setting_date_format: Datoformat
+ setting_time_format: Tidsformat
+ setting_cross_project_issue_relations: Tillat saksrelasjoner mellom prosjekter
+ setting_issue_list_default_columns: Standardkolonner vist i sakslisten
+ setting_repositories_encodings: Depot-tegnsett
+ setting_emails_footer: E-post-signatur
+ setting_protocol: Protokoll
+ setting_per_page_options: Alternativer, objekter pr. side
+ setting_user_format: Visningsformat, brukere
+ setting_activity_days_default: Dager vist på prosjektaktivitet
+ setting_display_subprojects_issues: Vis saker fra underprosjekter på hovedprosjekt som standard
+ setting_enabled_scm: Aktiviserte SCM
+
+ project_module_issue_tracking: Sakssporing
+ project_module_time_tracking: Tidssporing
+ project_module_news: Nyheter
+ project_module_documents: Dokumenter
+ project_module_files: Filer
+ project_module_wiki: Wiki
+ project_module_repository: Depot
+ project_module_boards: Forumer
+
+ label_user: Bruker
+ label_user_plural: Brukere
+ label_user_new: Ny bruker
+ label_project: Prosjekt
+ label_project_new: Nytt prosjekt
+ label_project_plural: Prosjekter
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Alle prosjekter
+ label_project_latest: Siste prosjekter
+ label_issue: Sak
+ label_issue_new: Ny sak
+ label_issue_plural: Saker
+ label_issue_view_all: Vis alle saker
+ label_issues_by: "Saker etter {{value}}"
+ label_issue_added: Sak lagt til
+ label_issue_updated: Sak oppdatert
+ label_document: Dokument
+ label_document_new: Nytt dokument
+ label_document_plural: Dokumenter
+ label_document_added: Dokument lagt til
+ label_role: Rolle
+ label_role_plural: Roller
+ label_role_new: Ny rolle
+ label_role_and_permissions: Roller og tillatelser
+ label_member: Medlem
+ label_member_new: Nytt medlem
+ label_member_plural: Medlemmer
+ label_tracker: Sakstype
+ label_tracker_plural: Sakstyper
+ label_tracker_new: Ny sakstype
+ label_workflow: Arbeidsflyt
+ label_issue_status: Saksstatus
+ label_issue_status_plural: Saksstatuser
+ label_issue_status_new: Ny status
+ label_issue_category: Sakskategori
+ label_issue_category_plural: Sakskategorier
+ label_issue_category_new: Ny kategori
+ label_custom_field: Eget felt
+ label_custom_field_plural: Egne felt
+ label_custom_field_new: Nytt eget felt
+ label_enumerations: Kodelister
+ label_enumeration_new: Ny verdi
+ label_information: Informasjon
+ label_information_plural: Informasjon
+ label_please_login: Vennlist logg inn
+ label_register: Registrer
+ label_password_lost: Mistet passord
+ label_home: Hjem
+ label_my_page: Min side
+ label_my_account: Min konto
+ label_my_projects: Mine prosjekter
+ label_administration: Administrasjon
+ label_login: Logg inn
+ label_logout: Logg ut
+ label_help: Hjelp
+ label_reported_issues: Rapporterte saker
+ label_assigned_to_me_issues: Saker tildelt meg
+ label_last_login: Sist innlogget
+ label_registered_on: Registrert
+ label_activity: Aktivitet
+ label_overall_activity: All aktivitet
+ label_new: Ny
+ label_logged_as: Innlogget som
+ label_environment: Miljø
+ label_authentication: Autentifikasjon
+ label_auth_source: Autentifikasjonsmodus
+ label_auth_source_new: Ny autentifikasjonmodus
+ label_auth_source_plural: Autentifikasjonsmoduser
+ label_subproject_plural: Underprosjekter
+ label_and_its_subprojects: "{{value}} og dets underprosjekter"
+ label_min_max_length: Min.-maks. lengde
+ label_list: Liste
+ label_date: Dato
+ label_integer: Heltall
+ label_float: Kommatall
+ label_boolean: Sann/usann
+ label_string: Tekst
+ label_text: Lang tekst
+ label_attribute: Attributt
+ label_attribute_plural: Attributter
+ label_download: "{{count}} Nedlasting"
+ label_download_plural: "{{count}} Nedlastinger"
+ label_no_data: Ingen data å vise
+ label_change_status: Endre status
+ label_history: Historikk
+ label_attachment: Fil
+ label_attachment_new: Ny fil
+ label_attachment_delete: Slett fil
+ label_attachment_plural: Filer
+ label_file_added: Fil lagt til
+ label_report: Rapport
+ label_report_plural: Rapporter
+ label_news: Nyheter
+ label_news_new: Legg til nyhet
+ label_news_plural: Nyheter
+ label_news_latest: Siste nyheter
+ label_news_view_all: Vis alle nyheter
+ label_news_added: Nyhet lagt til
+ label_change_log: Endringslogg
+ label_settings: Innstillinger
+ label_overview: Oversikt
+ label_version: Versjon
+ label_version_new: Ny versjon
+ label_version_plural: Versjoner
+ label_confirmation: Bekreftelse
+ label_export_to: Eksporter til
+ label_read: Leser...
+ label_public_projects: Offentlige prosjekt
+ label_open_issues: åpen
+ label_open_issues_plural: åpne
+ label_closed_issues: lukket
+ label_closed_issues_plural: lukkede
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Total
+ label_permissions: Godkjenninger
+ label_current_status: Nåværende status
+ label_new_statuses_allowed: Tillatte nye statuser
+ label_all: alle
+ label_none: ingen
+ label_nobody: ingen
+ label_next: Neste
+ label_previous: Forrige
+ label_used_by: Brukt av
+ label_details: Detaljer
+ label_add_note: Legg til notis
+ label_per_page: Pr. side
+ label_calendar: Kalender
+ label_months_from: måneder fra
+ label_gantt: Gantt
+ label_internal: Intern
+ label_last_changes: "siste {{count}} endringer"
+ label_change_view_all: Vis alle endringer
+ label_personalize_page: Tilpass denne siden
+ label_comment: Kommentar
+ label_comment_plural: Kommentarer
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Legg til kommentar
+ label_comment_added: Kommentar lagt til
+ label_comment_delete: Slett kommentar
+ label_query: Egen spørring
+ label_query_plural: Egne spørringer
+ label_query_new: Ny spørring
+ label_filter_add: Legg til filter
+ label_filter_plural: Filtre
+ label_equals: er
+ label_not_equals: er ikke
+ label_in_less_than: er mindre enn
+ label_in_more_than: in mer enn
+ label_in: i
+ label_today: idag
+ label_all_time: all tid
+ label_yesterday: i går
+ label_this_week: denne uken
+ label_last_week: sist uke
+ label_last_n_days: "siste {{count}} dager"
+ label_this_month: denne måneden
+ label_last_month: siste måned
+ label_this_year: dette året
+ label_date_range: Dato-spenn
+ label_less_than_ago: mindre enn dager siden
+ label_more_than_ago: mer enn dager siden
+ label_ago: dager siden
+ label_contains: inneholder
+ label_not_contains: ikke inneholder
+ label_day_plural: dager
+ label_repository: Depot
+ label_repository_plural: Depoter
+ label_browse: Utforsk
+ label_modification: "{{count}} endring"
+ label_modification_plural: "{{count}} endringer"
+ label_revision: Revisjon
+ label_revision_plural: Revisjoner
+ label_associated_revisions: Assosierte revisjoner
+ label_added: lagt til
+ label_modified: endret
+ label_deleted: slettet
+ label_latest_revision: Siste revisjon
+ label_latest_revision_plural: Siste revisjoner
+ label_view_revisions: Vis revisjoner
+ label_max_size: Maksimum størrelse
+ label_sort_highest: Flytt til toppen
+ label_sort_higher: Flytt opp
+ label_sort_lower: Flytt ned
+ label_sort_lowest: Flytt til bunnen
+ label_roadmap: Veikart
+ label_roadmap_due_in: "Frist om {{value}}"
+ label_roadmap_overdue: "{{value}} over fristen"
+ label_roadmap_no_issues: Ingen saker for denne versjonen
+ label_search: Søk
+ label_result_plural: Resultater
+ label_all_words: Alle ord
+ label_wiki: Wiki
+ label_wiki_edit: Wiki endring
+ label_wiki_edit_plural: Wiki endringer
+ label_wiki_page: Wiki-side
+ label_wiki_page_plural: Wiki-sider
+ label_index_by_title: Indekser etter tittel
+ label_index_by_date: Indekser etter dato
+ label_current_version: Gjeldende versjon
+ label_preview: Forhåndsvis
+ label_feed_plural: Feeder
+ label_changes_details: Detaljer om alle endringer
+ label_issue_tracking: Sakssporing
+ label_spent_time: Brukt tid
+ label_f_hour: "{{value}} time"
+ label_f_hour_plural: "{{value}} timer"
+ label_time_tracking: Tidssporing
+ label_change_plural: Endringer
+ label_statistics: Statistikk
+ label_commits_per_month: Innsendinger pr. måned
+ label_commits_per_author: Innsendinger pr. forfatter
+ label_view_diff: Vis forskjeller
+ label_diff_inline: i teksten
+ label_diff_side_by_side: side ved side
+ label_options: Alternativer
+ label_copy_workflow_from: Kopier arbeidsflyt fra
+ label_permissions_report: Godkjenningsrapport
+ label_watched_issues: Overvåkede saker
+ label_related_issues: Relaterte saker
+ label_applied_status: Gitt status
+ label_loading: Laster...
+ label_relation_new: Ny relasjon
+ label_relation_delete: Slett relasjon
+ label_relates_to: relatert til
+ label_duplicates: dupliserer
+ label_duplicated_by: duplisert av
+ label_blocks: blokkerer
+ label_blocked_by: blokkert av
+ label_precedes: kommer før
+ label_follows: følger
+ label_end_to_start: slutt til start
+ label_end_to_end: slutt til slutt
+ label_start_to_start: start til start
+ label_start_to_end: start til slutt
+ label_stay_logged_in: Hold meg innlogget
+ label_disabled: avslått
+ label_show_completed_versions: Vis ferdige versjoner
+ label_me: meg
+ label_board: Forum
+ label_board_new: Nytt forum
+ label_board_plural: Forumer
+ label_topic_plural: Emner
+ label_message_plural: Meldinger
+ label_message_last: Siste melding
+ label_message_new: Ny melding
+ label_message_posted: Melding lagt til
+ label_reply_plural: Svar
+ label_send_information: Send kontoinformasjon til brukeren
+ label_year: År
+ label_month: Måned
+ label_week: Uke
+ label_date_from: Fra
+ label_date_to: Til
+ label_language_based: Basert på brukerens språk
+ label_sort_by: "Sorter etter {{value}}"
+ label_send_test_email: Send en e-post-test
+ label_feeds_access_key_created_on: "RSS tilgangsnøkkel opprettet for {{value}} siden"
+ label_module_plural: Moduler
+ label_added_time_by: "Lagt til av {{author}} for {{age}} siden"
+ label_updated_time: "Oppdatert for {{value}} siden"
+ label_jump_to_a_project: Gå til et prosjekt...
+ label_file_plural: Filer
+ label_changeset_plural: Endringssett
+ label_default_columns: Standardkolonner
+ label_no_change_option: (Ingen endring)
+ label_bulk_edit_selected_issues: Samlet endring av valgte saker
+ label_theme: Tema
+ label_default: Standard
+ label_search_titles_only: Søk bare i titler
+ label_user_mail_option_all: "For alle hendelser på mine prosjekter"
+ label_user_mail_option_selected: "For alle hendelser på valgte prosjekt..."
+ label_user_mail_option_none: "Bare for ting jeg overvåker eller er involvert i"
+ label_user_mail_no_self_notified: "Jeg vil ikke bli varslet om endringer jeg selv gjør"
+ label_registration_activation_by_email: kontoaktivering pr. e-post
+ label_registration_manual_activation: manuell kontoaktivering
+ label_registration_automatic_activation: automatisk kontoaktivering
+ label_display_per_page: "Pr. side: {{value}}"
+ label_age: Alder
+ label_change_properties: Endre egenskaper
+ label_general: Generell
+ label_more: Mer
+ label_scm: SCM
+ label_plugins: Tillegg
+ label_ldap_authentication: LDAP-autentifikasjon
+ label_downloads_abbr: Nedl.
+ label_optional_description: Valgfri beskrivelse
+ label_add_another_file: Legg til en fil til
+ label_preferences: Brukerinnstillinger
+ label_chronological_order: I kronologisk rekkefølge
+ label_reverse_chronological_order: I omvendt kronologisk rekkefølge
+ label_planning: Planlegging
+
+ button_login: Logg inn
+ button_submit: Send
+ button_save: Lagre
+ button_check_all: Merk alle
+ button_uncheck_all: Avmerk alle
+ button_delete: Slett
+ button_create: Opprett
+ button_test: Test
+ button_edit: Endre
+ button_add: Legg til
+ button_change: Endre
+ button_apply: Bruk
+ button_clear: Nullstill
+ button_lock: Lås
+ button_unlock: Lås opp
+ button_download: Last ned
+ button_list: Liste
+ button_view: Vis
+ button_move: Flytt
+ button_back: Tilbake
+ button_cancel: Avbryt
+ button_activate: Aktiver
+ button_sort: Sorter
+ button_log_time: Logg tid
+ button_rollback: Rull tilbake til denne versjonen
+ button_watch: Overvåk
+ button_unwatch: Stopp overvåkning
+ button_reply: Svar
+ button_archive: Arkiver
+ button_unarchive: Gjør om arkivering
+ button_reset: Nullstill
+ button_rename: Endre navn
+ button_change_password: Endre passord
+ button_copy: Kopier
+ button_annotate: Notér
+ button_update: Oppdater
+ button_configure: Konfigurer
+
+ status_active: aktiv
+ status_registered: registrert
+ status_locked: låst
+
+ text_select_mail_notifications: Velg hendelser som skal varsles med e-post.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 betyr ingen begrensning
+ text_project_destroy_confirmation: Er du sikker på at du vil slette dette prosjekter og alle relatert data ?
+ text_subprojects_destroy_warning: "Underprojekt(ene): {{value}} vil også bli slettet."
+ text_workflow_edit: Velg en rolle og en sakstype for å endre arbeidsflyten
+ text_are_you_sure: Er du sikker ?
+ text_tip_task_begin_day: oppgaven starter denne dagen
+ text_tip_task_end_day: oppgaven avsluttes denne dagen
+ text_tip_task_begin_end_day: oppgaven starter og avsluttes denne dagen
+ text_project_identifier_info: 'Små bokstaver (a-z), nummer og bindestrek tillatt.<br />Identifikatoren kan ikke endres etter den er lagret.'
+ text_caracters_maximum: "{{count}} tegn maksimum."
+ text_caracters_minimum: "Må være minst {{count}} tegn langt."
+ text_length_between: "Lengde mellom {{min}} og {{max}} tegn."
+ text_tracker_no_workflow: Ingen arbeidsflyt definert for denne sakstypen
+ text_unallowed_characters: Ugyldige tegn
+ text_comma_separated: Flere verdier tillat (kommaseparert).
+ text_issues_ref_in_commit_messages: Referering og retting av saker i innsendingsmelding
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{author}}."
+ text_wiki_destroy_confirmation: Er du sikker på at du vil slette denne wikien og alt innholdet ?
+ text_issue_category_destroy_question: "Noen saker ({{count}}) er lagt til i denne kategorien. Hva vil du gjøre ?"
+ text_issue_category_destroy_assignments: Fjern bruk av kategorier
+ text_issue_category_reassign_to: Overfør sakene til denne kategorien
+ text_user_mail_option: "For ikke-valgte prosjekter vil du bare motta varsling om ting du overvåker eller er involveret i (eks. saker du er forfatter av eller er tildelt)."
+ text_no_configuration_data: "Roller, arbeidsflyt, sakstyper og -statuser er ikke konfigurert enda.\nDet anbefales sterkt å laste inn standardkonfigurasjonen. Du vil kunne endre denne etter den er innlastet."
+ text_load_default_configuration: Last inn standardkonfigurasjonen
+ text_status_changed_by_changeset: "Brukt i endringssett {{value}}."
+ text_issues_destroy_confirmation: 'Er du sikker på at du vil slette valgte sak(er) ?'
+ text_select_project_modules: 'Velg moduler du vil aktivere for dette prosjektet:'
+ text_default_administrator_account_changed: Standard administrator-konto er endret
+ text_file_repository_writable: Fil-arkivet er skrivbart
+ text_rmagick_available: RMagick er tilgjengelig (valgfritt)
+ text_destroy_time_entries_question: "{{hours}} timer er ført på sakene du er i ferd med å slette. Hva vil du gjøre ?"
+ text_destroy_time_entries: Slett førte timer
+ text_assign_time_entries_to_project: Overfør førte timer til prosjektet
+ text_reassign_time_entries: 'Overfør førte timer til denne saken:'
+ text_user_wrote: "{{value}} skrev:"
+
+ default_role_manager: Leder
+ default_role_developper: Utvikler
+ default_role_reporter: Rapportør
+ default_tracker_bug: Feil
+ default_tracker_feature: Funksjon
+ default_tracker_support: Support
+ default_issue_status_new: Ny
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Avklart
+ default_issue_status_feedback: Tilbakemelding
+ default_issue_status_closed: Lukket
+ default_issue_status_rejected: Avvist
+ default_doc_category_user: Bruker-dokumentasjon
+ default_doc_category_tech: Teknisk dokumentasjon
+ default_priority_low: Lav
+ default_priority_normal: Normal
+ default_priority_high: Høy
+ default_priority_urgent: Haster
+ default_priority_immediate: Omgående
+ default_activity_design: Design
+ default_activity_development: Utvikling
+
+ enumeration_issue_priorities: Sakssprioriteringer
+ enumeration_doc_categories: Dokument-kategorier
+ enumeration_activities: Aktiviteter (tidssporing)
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: API key
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: Parent page
+ label_issue_watchers: Watchers
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: Quote
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: renamed
+ label_copied: copied
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: Example
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Polish translations for Ruby on Rails
+# by Jacek Becela (jacek.becela@gmail.com, http://github.com/ncr)
+
+pl:
+ number:
+ format:
+ separator: ","
+ delimiter: " "
+ precision: 2
+ currency:
+ format:
+ format: "%n %u"
+ unit: "PLN"
+ percentage:
+ format:
+ delimiter: ""
+ precision:
+ format:
+ delimiter: ""
+ human:
+ format:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "B"
+ other: "B"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%d %b"
+ long: "%d %B %Y"
+
+ day_names: [Niedziela, Poniedziałek, Wtorek, Środa, Czwartek, Piątek, Sobota]
+ abbr_day_names: [nie, pon, wto, śro, czw, pia, sob]
+
+ month_names: [~, Styczeń, Luty, Marzec, Kwiecień, Maj, Czerwiec, Lipiec, Sierpień, Wrzesień, Październik, Listopad, Grudzień]
+ abbr_month_names: [~, sty, lut, mar, kwi, maj, cze, lip, sie, wrz, paź, lis, gru]
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y, %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b, %H:%M"
+ long: "%d %B %Y, %H:%M"
+ am: "przed południem"
+ pm: "po południu"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "pół minuty"
+ less_than_x_seconds:
+ one: "mniej niż sekundę"
+ few: "mniej niż {{count}} sekundy"
+ other: "mniej niż {{count}} sekund"
+ x_seconds:
+ one: "sekundę"
+ few: "{{count}} sekundy"
+ other: "{{count}} sekund"
+ less_than_x_minutes:
+ one: "mniej niż minutę"
+ few: "mniej niż {{count}} minuty"
+ other: "mniej niż {{count}} minut"
+ x_minutes:
+ one: "minutę"
+ few: "{{count}} minuty"
+ other: "{{count}} minut"
+ about_x_hours:
+ one: "około godziny"
+ other: "około {{count}} godzin"
+ x_days:
+ one: "1 dzień"
+ other: "{{count}} dni"
+ about_x_months:
+ one: "około miesiąca"
+ other: "około {{count}} miesięcy"
+ x_months:
+ one: "1 miesiąc"
+ few: "{{count}} miesiące"
+ other: "{{count}} miesięcy"
+ about_x_years:
+ one: "około roku"
+ other: "około {{count}} lat"
+ over_x_years:
+ one: "ponad rok"
+ few: "ponad {{count}} lata"
+ other: "ponad {{count}} lat"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "{{model}} nie został zachowany z powodu jednego błędu"
+ other: "{{model}} nie został zachowany z powodu {{count}} błędów"
+ body: "Błędy dotyczą następujących pól:"
+ messages:
+ inclusion: "nie znajduje się na liście dopuszczalnych wartości"
+ exclusion: "znajduje się na liście zabronionych wartości"
+ invalid: "jest nieprawidłowe"
+ confirmation: "nie zgadza się z potwierdzeniem"
+ accepted: "musi być zaakceptowane"
+ empty: "nie może być puste"
+ blank: "nie może być puste"
+ too_long: "jest za długie (maksymalnie {{count}} znaków)"
+ too_short: "jest za krótkie (minimalnie {{count}} znaków)"
+ wrong_length: "jest nieprawidłowej długości (powinna wynosić {{count}} znaków)"
+ taken: "jest już zajęte"
+ not_a_number: "nie jest liczbą"
+ greater_than: "musi być większe niż {{count}}"
+ greater_than_or_equal_to: "musi być większe lub równe {{count}}"
+ equal_to: "musi być równe {{count}}"
+ less_than: "musie być mniejsze niż {{count}}"
+ less_than_or_equal_to: "musi być mniejsze lub równe {{count}}"
+ odd: "musi być nieparzyste"
+ even: "musi być parzyste"
+ greater_than_start_date: "musi być większe niż początkowa data"
+ not_same_project: "nie należy do tego samego projektu"
+ circular_dependency: "Ta relacja może wytworzyć kołową zależność"
+
+ support:
+ array:
+ sentence_connector: "i"
+ skip_last_comma: true
+
+ # Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BFidea...
+ # Best regards from Lublin@Poland :-)
+ # PL translation by Mariusz@Olejnik.net,
+ actionview_instancetag_blank_option: Proszę wybierz
+
+ button_activate: Aktywuj
+ button_add: Dodaj
+ button_annotate: Adnotuj
+ button_apply: Ustaw
+ button_archive: Archiwizuj
+ button_back: Wstecz
+ button_cancel: Anuluj
+ button_change: Zmień
+ button_change_password: Zmień hasło
+ button_check_all: Zaznacz wszystko
+ button_clear: Wyczyść
+ button_configure: Konfiguruj
+ button_copy: Kopia
+ button_create: Stwórz
+ button_delete: Usuń
+ button_download: Pobierz
+ button_edit: Edytuj
+ button_list: Lista
+ button_lock: Zablokuj
+ button_log_time: Log czasu
+ button_login: Login
+ button_move: Przenieś
+ button_quote: Cytuj
+ button_rename: Zmień nazwę
+ button_reply: Odpowiedz
+ button_reset: Resetuj
+ button_rollback: Przywróc do tej wersji
+ button_save: Zapisz
+ button_sort: Sortuj
+ button_submit: Wyślij
+ button_test: Testuj
+ button_unarchive: Przywróc z archiwum
+ button_uncheck_all: Odznacz wszystko
+ button_unlock: Odblokuj
+ button_unwatch: Nie obserwuj
+ button_update: Uaktualnij
+ button_view: Pokaż
+ button_watch: Obserwuj
+ default_activity_design: Projektowanie
+ default_activity_development: Rozwój
+ default_doc_category_tech: Dokumentacja techniczna
+ default_doc_category_user: Dokumentacja użytkownika
+ default_issue_status_in_progress: W Toku
+ default_issue_status_closed: Zamknięty
+ default_issue_status_feedback: Odpowiedź
+ default_issue_status_new: Nowy
+ default_issue_status_rejected: Odrzucony
+ default_issue_status_resolved: Rozwiązany
+ default_priority_high: Wysoki
+ default_priority_immediate: Natychmiastowy
+ default_priority_low: Niski
+ default_priority_normal: Normalny
+ default_priority_urgent: Pilny
+ default_role_developper: Programista
+ default_role_manager: Kierownik
+ default_role_reporter: Wprowadzajacy
+ default_tracker_bug: Błąd
+ default_tracker_feature: Zadanie
+ default_tracker_support: Wsparcie
+ enumeration_activities: Działania (śledzenie czasu)
+ enumeration_doc_categories: Kategorie dokumentów
+ enumeration_issue_priorities: Priorytety zagadnień
+ error_can_t_load_default_data: "Domyślna konfiguracja nie może być załadowana: {{value}}"
+ error_issue_not_found_in_project: 'Zaganienie nie zostało znalezione lub nie należy do tego projektu'
+ error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji."
+ error_scm_command_failed: "Wystąpił błąd przy próbie dostępu do repozytorium: {{value}}"
+ error_scm_not_found: "Obiekt lub wersja nie zostały znalezione w repozytorium."
+ field_account: Konto
+ field_activity: Aktywność
+ field_admin: Administrator
+ field_assignable: Zagadnienia mogą być przypisane do tej roli
+ field_assigned_to: Przydzielony do
+ field_attr_firstname: Imię atrybut
+ field_attr_lastname: Nazwisko atrybut
+ field_attr_login: Login atrybut
+ field_attr_mail: Email atrybut
+ field_auth_source: Tryb identyfikacji
+ field_author: Autor
+ field_base_dn: Base DN
+ field_category: Kategoria
+ field_column_names: Nazwy kolumn
+ field_comments: Komentarz
+ field_comments_sorting: Pokazuj komentarze
+ field_created_on: Stworzone
+ field_default_value: Domyślny
+ field_delay: Opóźnienie
+ field_description: Opis
+ field_done_ratio: % Wykonane
+ field_downloads: Pobrań
+ field_due_date: Data oddania
+ field_effective_date: Data
+ field_estimated_hours: Szacowany czas
+ field_field_format: Format
+ field_filename: Plik
+ field_filesize: Rozmiar
+ field_firstname: Imię
+ field_fixed_version: Wersja docelowa
+ field_hide_mail: Ukryj mój adres email
+ field_homepage: Strona www
+ field_host: Host
+ field_hours: Godzin
+ field_identifier: Identifikator
+ field_is_closed: Zagadnienie zamknięte
+ field_is_default: Domyślny status
+ field_is_filter: Atrybut filtrowania
+ field_is_for_all: Dla wszystkich projektów
+ field_is_in_chlog: Zagadnienie pokazywane w zapisie zmian
+ field_is_in_roadmap: Zagadnienie pokazywane na mapie
+ field_is_public: Publiczny
+ field_is_required: Wymagane
+ field_issue: Zagadnienie
+ field_issue_to: Powiązania zagadnienia
+ field_language: Język
+ field_last_login_on: Ostatnie połączenie
+ field_lastname: Nazwisko
+ field_login: Login
+ field_mail: Email
+ field_mail_notification: Powiadomienia Email
+ field_max_length: Maksymalna długość
+ field_min_length: Minimalna długość
+ field_name: Nazwa
+ field_new_password: Nowe hasło
+ field_notes: Notatki
+ field_onthefly: Tworzenie użytkownika w locie
+ field_parent: Nadprojekt
+ field_parent_title: Strona rodzica
+ field_password: Hasło
+ field_password_confirmation: Potwierdzenie
+ field_port: Port
+ field_possible_values: Możliwe wartości
+ field_priority: Priorytet
+ field_project: Projekt
+ field_redirect_existing_links: Przekierowanie istniejących odnośników
+ field_regexp: Wyrażenie regularne
+ field_role: Rola
+ field_searchable: Przeszukiwalne
+ field_spent_on: Data
+ field_start_date: Start
+ field_start_page: Strona startowa
+ field_status: Status
+ field_subject: Temat
+ field_subproject: Podprojekt
+ field_summary: Podsumowanie
+ field_time_zone: Strefa czasowa
+ field_title: Tytuł
+ field_tracker: Typ zagadnienia
+ field_type: Typ
+ field_updated_on: Zmienione
+ field_url: URL
+ field_user: Użytkownik
+ field_value: Wartość
+ field_version: Wersja
+ field_vf_personnel: Personel
+ field_vf_watcher: Obserwator
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_csv_separator: ','
+ general_first_day_of_week: '1'
+ general_lang_name: 'Polski'
+ general_pdf_encoding: UTF-8
+ general_text_No: 'Nie'
+ general_text_Yes: 'Tak'
+ general_text_no: 'nie'
+ general_text_yes: 'tak'
+ gui_validation_error: 1 błąd
+ gui_validation_error_plural234: "{{count}} błędy"
+ gui_validation_error_plural5: "{{count}} błędów"
+ gui_validation_error_plural: "{{count}} błędów"
+ label_activity: Aktywność
+ label_add_another_file: Dodaj kolejny plik
+ label_add_note: Dodaj notatkę
+ label_added: dodane
+ label_added_time_by: "Dodane przez {{author}} {{age}} temu"
+ label_administration: Administracja
+ label_age: Wiek
+ label_ago: dni temu
+ label_all: wszystko
+ label_all_time: cały czas
+ label_all_words: Wszystkie słowa
+ label_and_its_subprojects: "{{value}} i podprojekty"
+ label_applied_status: Stosowany status
+ label_assigned_to_me_issues: Zagadnienia przypisane do mnie
+ label_associated_revisions: Skojarzone rewizje
+ label_attachment: Plik
+ label_attachment_delete: Usuń plik
+ label_attachment_new: Nowy plik
+ label_attachment_plural: Pliki
+ label_attribute: Atrybut
+ label_attribute_plural: Atrybuty
+ label_auth_source: Tryb identyfikacji
+ label_auth_source_new: Nowy tryb identyfikacji
+ label_auth_source_plural: Tryby identyfikacji
+ label_authentication: Identyfikacja
+ label_blocked_by: zablokowane przez
+ label_blocks: blokady
+ label_board: Forum
+ label_board_new: Nowe forum
+ label_board_plural: Fora
+ label_boolean: Wartość logiczna
+ label_browse: Przegląd
+ label_bulk_edit_selected_issues: Zbiorowa edycja zagadnień
+ label_calendar: Kalendarz
+ label_change_log: Lista zmian
+ label_change_plural: Zmiany
+ label_change_properties: Zmień właściwości
+ label_change_status: Status zmian
+ label_change_view_all: Pokaż wszystkie zmiany
+ label_changes_details: Szczegóły wszystkich zmian
+ label_changeset_plural: Zestawienia zmian
+ label_chronological_order: W kolejności chronologicznej
+ label_closed_issues: zamknięte
+ label_closed_issues_plural234: zamknięte
+ label_closed_issues_plural5: zamknięte
+ label_closed_issues_plural: zamknięte
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_comment: Komentarz
+ label_comment_add: Dodaj komentarz
+ label_comment_added: Komentarz dodany
+ label_comment_delete: Usuń komentarze
+ label_comment_plural234: Komentarze
+ label_comment_plural5: Komentarze
+ label_comment_plural: Komentarze
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_commits_per_author: Zatwierdzenia według autorów
+ label_commits_per_month: Zatwierdzenia według miesięcy
+ label_confirmation: Potwierdzenie
+ label_contains: zawiera
+ label_copied: skopiowano
+ label_copy_workflow_from: Kopiuj przepływ z
+ label_current_status: Obecny status
+ label_current_version: Obecna wersja
+ label_custom_field: Dowolne pole
+ label_custom_field_new: Nowe dowolne pole
+ label_custom_field_plural: Dowolne pola
+ label_date: Data
+ label_date_from: Z
+ label_date_range: Zakres datowy
+ label_date_to: Do
+ label_day_plural: dni
+ label_default: Domyślne
+ label_default_columns: Domyślne kolumny
+ label_deleted: usunięte
+ label_details: Szczegóły
+ label_diff_inline: w linii
+ label_diff_side_by_side: obok siebie
+ label_disabled: zablokowany
+ label_display_per_page: "Na stronę: {{value}}"
+ label_document: Dokument
+ label_document_added: Dodano dokument
+ label_document_new: Nowy dokument
+ label_document_plural: Dokumenty
+ label_download: "{{count}} Pobranie"
+ label_download_plural234: "{{count}} Pobrania"
+ label_download_plural5: "{{count}} Pobrań"
+ label_download_plural: "{{count}} Pobrania"
+ label_downloads_abbr: Pobieranie
+ label_duplicated_by: zduplikowane przez
+ label_duplicates: duplikaty
+ label_end_to_end: koniec do końca
+ label_end_to_start: koniec do początku
+ label_enumeration_new: Nowa wartość
+ label_enumerations: Wyliczenia
+ label_environment: Środowisko
+ label_equals: równa się
+ label_example: Przykład
+ label_export_to: Eksportuj do
+ label_f_hour: "{{value}} godzina"
+ label_f_hour_plural: "{{value}} godzin"
+ label_feed_plural: Ilość RSS
+ label_feeds_access_key_created_on: "Klucz dostępu RSS stworzony {{value}} dni temu"
+ label_file_added: Dodano plik
+ label_file_plural: Pliki
+ label_filter_add: Dodaj filtr
+ label_filter_plural: Filtry
+ label_float: Liczba rzeczywista
+ label_follows: następuje po
+ label_gantt: Gantt
+ label_general: Ogólne
+ label_generate_key: Wygeneruj klucz
+ label_help: Pomoc
+ label_history: Historia
+ label_home: Główna
+ label_in: w
+ label_in_less_than: mniejsze niż
+ label_in_more_than: większe niż
+ label_incoming_emails: Przychodząca poczta elektroniczna
+ label_index_by_date: Indeks wg daty
+ label_index_by_title: Indeks
+ label_information: Informacja
+ label_information_plural: Informacje
+ label_integer: Liczba całkowita
+ label_internal: Wewnętrzny
+ label_issue: Zagadnienie
+ label_issue_added: Dodano zagadnienie
+ label_issue_category: Kategoria zagadnienia
+ label_issue_category_new: Nowa kategoria
+ label_issue_category_plural: Kategorie zagadnień
+ label_issue_new: Nowe zagadnienie
+ label_issue_plural: Zagadnienia
+ label_issue_status: Status zagadnienia
+ label_issue_status_new: Nowy status
+ label_issue_status_plural: Statusy zagadnień
+ label_issue_tracking: Śledzenie zagadnień
+ label_issue_updated: Uaktualniono zagadnienie
+ label_issue_view_all: Zobacz wszystkie zagadnienia
+ label_issue_watchers: Obserwatorzy
+ label_issues_by: "Zagadnienia wprowadzone przez {{value}}"
+ label_jump_to_a_project: Skocz do projektu...
+ label_language_based: Na podstawie języka
+ label_last_changes: "ostatnie {{count}} zmian"
+ label_last_login: Ostatnie połączenie
+ label_last_month: ostatni miesiąc
+ label_last_n_days: "ostatnie {{count}} dni"
+ label_last_week: ostatni tydzień
+ label_latest_revision: Najnowsza rewizja
+ label_latest_revision_plural: Najnowsze rewizje
+ label_ldap_authentication: Autoryzacja LDAP
+ label_less_than_ago: dni mniej
+ label_list: Lista
+ label_loading: Ładowanie...
+ label_logged_as: Zalogowany jako
+ label_login: Login
+ label_logout: Wylogowanie
+ label_max_size: Maksymalny rozmiar
+ label_me: ja
+ label_member: Uczestnik
+ label_member_new: Nowy uczestnik
+ label_member_plural: Uczestnicy
+ label_message_last: Ostatnia wiadomość
+ label_message_new: Nowa wiadomość
+ label_message_plural: Wiadomości
+ label_message_posted: Dodano wiadomość
+ label_min_max_length: Min - Maks długość
+ label_modification: "{{count}} modyfikacja"
+ label_modification_plural234: "{{count}} modyfikacje"
+ label_modification_plural5: "{{count}} modyfikacji"
+ label_modification_plural: "{{count}} modyfikacje"
+ label_modified: zmodyfikowane
+ label_module_plural: Moduły
+ label_month: Miesiąc
+ label_months_from: miesiące od
+ label_more: Więcej
+ label_more_than_ago: dni więcej
+ label_my_account: Moje konto
+ label_my_page: Moja strona
+ label_my_projects: Moje projekty
+ label_new: Nowy
+ label_new_statuses_allowed: Uprawnione nowe statusy
+ label_news: Komunikat
+ label_news_added: Dodano komunikat
+ label_news_latest: Ostatnie komunikaty
+ label_news_new: Dodaj komunikat
+ label_news_plural: Komunikaty
+ label_news_view_all: Pokaż wszystkie komunikaty
+ label_next: Następne
+ label_no_change_option: (Bez zmian)
+ label_no_data: Brak danych do pokazania
+ label_nobody: nikt
+ label_none: brak
+ label_not_contains: nie zawiera
+ label_not_equals: różni się
+ label_open_issues: otwarte
+ label_open_issues_plural234: otwarte
+ label_open_issues_plural5: otwarte
+ label_open_issues_plural: otwarte
+ label_optional_description: Opcjonalny opis
+ label_options: Opcje
+ label_overall_activity: Ogólna aktywność
+ label_overview: Przegląd
+ label_password_lost: Zapomniane hasło
+ label_per_page: Na stronę
+ label_permissions: Uprawnienia
+ label_permissions_report: Raport uprawnień
+ label_personalize_page: Personalizuj tą stronę
+ label_planning: Planowanie
+ label_please_login: Zaloguj się
+ label_plugins: Wtyczki
+ label_precedes: poprzedza
+ label_preferences: Preferencje
+ label_preview: Podgląd
+ label_previous: Poprzednie
+ label_project: Projekt
+ label_project_all: Wszystkie projekty
+ label_project_latest: Ostatnie projekty
+ label_project_new: Nowy projekt
+ label_project_plural234: Projekty
+ label_project_plural5: Projekty
+ label_project_plural: Projekty
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_public_projects: Projekty publiczne
+ label_query: Kwerenda
+ label_query_new: Nowa kwerenda
+ label_query_plural: Kwerendy
+ label_read: Czytanie...
+ label_register: Rejestracja
+ label_registered_on: Zarejestrowany
+ label_registration_activation_by_email: aktywacja konta przez e-mail
+ label_registration_automatic_activation: automatyczna aktywacja kont
+ label_registration_manual_activation: manualna aktywacja kont
+ label_related_issues: Powiązane zagadnienia
+ label_relates_to: powiązane z
+ label_relation_delete: Usuń powiązanie
+ label_relation_new: Nowe powiązanie
+ label_renamed: przemianowano
+ label_reply_plural: Odpowiedzi
+ label_report: Raport
+ label_report_plural: Raporty
+ label_reported_issues: Wprowadzone zagadnienia
+ label_repository: Repozytorium
+ label_repository_plural: Repozytoria
+ label_result_plural: Rezultatów
+ label_reverse_chronological_order: W kolejności odwrotnej do chronologicznej
+ label_revision: Rewizja
+ label_revision_plural: Rewizje
+ label_roadmap: Mapa
+ label_roadmap_due_in: W czasie
+ label_roadmap_no_issues: Brak zagadnień do tej wersji
+ label_roadmap_overdue: "{{value}} spóźnienia"
+ label_role: Rola
+ label_role_and_permissions: Role i Uprawnienia
+ label_role_new: Nowa rola
+ label_role_plural: Role
+ label_scm: SCM
+ label_search: Szukaj
+ label_search_titles_only: Przeszukuj tylko tytuły
+ label_send_information: Wyślij informację użytkownikowi
+ label_send_test_email: Wyślij próbny email
+ label_settings: Ustawienia
+ label_show_completed_versions: Pokaż kompletne wersje
+ label_sort_by: "Sortuj po {{value}}"
+ label_sort_higher: Do góry
+ label_sort_highest: Przesuń na górę
+ label_sort_lower: Do dołu
+ label_sort_lowest: Przesuń na dół
+ label_spent_time: Spędzony czas
+ label_start_to_end: początek do końca
+ label_start_to_start: początek do początku
+ label_statistics: Statystyki
+ label_stay_logged_in: Pozostań zalogowany
+ label_string: Tekst
+ label_subproject_plural: Podprojekty
+ label_text: Długi tekst
+ label_theme: Temat
+ label_this_month: ten miesiąc
+ label_this_week: ten tydzień
+ label_this_year: ten rok
+ label_time_tracking: Śledzenie czasu
+ label_today: dzisiaj
+ label_topic_plural: Tematy
+ label_total: Ogółem
+ label_tracker: Typ zagadnienia
+ label_tracker_new: Nowy typ zagadnienia
+ label_tracker_plural: Typy zagadnień
+ label_updated_time: "Zaktualizowane {{value}} temu"
+ label_used_by: Używane przez
+ label_user: Użytkownik
+ label_user_mail_no_self_notified: "Nie chcę powiadomień o zmianach, które sam wprowadzam."
+ label_user_mail_option_all: "Dla każdego zdarzenia w każdym moim projekcie"
+ label_user_mail_option_none: "Tylko to co obserwuje lub w czym biorę udział"
+ label_user_mail_option_selected: "Tylko dla każdego zdarzenia w wybranych projektach..."
+ label_user_new: Nowy użytkownik
+ label_user_plural: Użytkownicy
+ label_version: Wersja
+ label_version_new: Nowa wersja
+ label_version_plural: Wersje
+ label_view_diff: Pokaż różnice
+ label_view_revisions: Pokaż rewizje
+ label_watched_issues: Obserwowane zagadnienia
+ label_week: Tydzień
+ label_wiki: Wiki
+ label_wiki_edit: Edycja wiki
+ label_wiki_edit_plural: Edycje wiki
+ label_wiki_page: Strona wiki
+ label_wiki_page_plural: Strony wiki
+ label_workflow: Przepływ
+ label_year: Rok
+ label_yesterday: wczoraj
+ mail_body_account_activation_request: "Zarejestrowano nowego użytkownika: ({{value}}). Konto oczekuje na twoje zatwierdzenie:"
+ mail_body_account_information: Twoje konto
+ mail_body_account_information_external: "Możesz użyć twojego {{value}} konta do zalogowania."
+ mail_body_lost_password: 'W celu zmiany swojego hasła użyj poniższego odnośnika:'
+ mail_body_register: 'W celu aktywacji Twojego konta, użyj poniższego odnośnika:'
+ mail_body_reminder: "Wykaz przypisanych do Ciebie zagadnień, których termin wypada w ciągu następnych {{count}} dni"
+ mail_subject_account_activation_request: "Zapytanie aktywacyjne konta {{value}}"
+ mail_subject_lost_password: "Twoje hasło do {{value}}"
+ mail_subject_register: "Aktywacja konta w {{value}}"
+ mail_subject_reminder: "Uwaga na terminy, masz zagadnienia do obsłużenia w ciągu następnych {{count}} dni!"
+ notice_account_activated: Twoje konto zostało aktywowane. Możesz się zalogować.
+ notice_account_invalid_creditentials: Zły użytkownik lub hasło
+ notice_account_lost_email_sent: Email z instrukcjami zmiany hasła został wysłany do Ciebie.
+ notice_account_password_updated: Hasło prawidłowo zmienione.
+ notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora."
+ notice_account_register_done: Konto prawidłowo stworzone.
+ notice_account_unknown_email: Nieznany użytkownik.
+ notice_account_updated: Konto prawidłowo zaktualizowane.
+ notice_account_wrong_password: Złe hasło
+ notice_can_t_change_password: To konto ma zewnętrzne źródło identyfikacji. Nie możesz zmienić hasła.
+ notice_default_data_loaded: Domyślna konfiguracja została pomyślnie załadowana.
+ notice_email_error: "Wystąpił błąd w trakcie wysyłania maila ({{value}})"
+ notice_email_sent: "Email został wysłany do {{value}}"
+ notice_failed_to_save_issues: "Błąd podczas zapisu zagadnień {{count}} z {{total}} zaznaczonych: {{ids}}."
+ notice_feeds_access_key_reseted: Twój klucz dostępu RSS został zrestetowany.
+ notice_file_not_found: Strona do której próbujesz się dostać nie istnieje lub została usunięta.
+ notice_locking_conflict: Dane poprawione przez innego użytkownika.
+ notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować."
+ notice_not_authorized: Nie jesteś autoryzowany by zobaczyć stronę.
+ notice_successful_connection: Udane nawiązanie połączenia.
+ notice_successful_create: Utworzenie zakończone sukcesem.
+ notice_successful_delete: Usunięcie zakończone sukcesem.
+ notice_successful_update: Uaktualnienie zakończone sukcesem.
+ notice_unable_delete_version: Nie można usunąć wersji
+ permission_add_issue_notes: Dodawanie notatek
+ permission_add_issue_watchers: Dodawanie obserwatorów
+ permission_add_issues: Dodawanie zagadnień
+ permission_add_messages: Dodawanie wiadomości
+ permission_browse_repository: Przeglądanie repozytorium
+ permission_comment_news: Komentowanie komunikatów
+ permission_commit_access: Wykonywanie zatwierdzeń
+ permission_delete_issues: Usuwanie zagadnień
+ permission_delete_messages: Usuwanie wiadomości
+ permission_delete_wiki_pages: Usuwanie stron wiki
+ permission_delete_wiki_pages_attachments: Usuwanie załączników
+ permission_delete_own_messages: Usuwanie własnych wiadomości
+ permission_edit_issue_notes: Edycja notatek
+ permission_edit_issues: Edycja zagadnień
+ permission_edit_messages: Edycja wiadomości
+ permission_edit_own_issue_notes: Edycja własnych notatek
+ permission_edit_own_messages: Edycja własnych wiadomości
+ permission_edit_own_time_entries: Edycja własnego logu czasu
+ permission_edit_project: Edycja projektów
+ permission_edit_time_entries: Edycja logów czasu
+ permission_edit_wiki_pages: Edycja stron wiki
+ permission_log_time: Zapisywanie spędzonego czasu
+ permission_manage_boards: Zarządzanie forami
+ permission_manage_categories: Zarządzanie kategoriami zaganień
+ permission_manage_documents: Zarządzanie dokumentami
+ permission_manage_files: Zarządzanie plikami
+ permission_manage_issue_relations: Zarządzanie powiązaniami zagadnień
+ permission_manage_members: Zarządzanie uczestnikami
+ permission_manage_news: Zarządzanie komunikatami
+ permission_manage_public_queries: Zarządzanie publicznymi kwerendami
+ permission_manage_repository: Zarządzanie repozytorium
+ permission_manage_versions: Zarządzanie wersjami
+ permission_manage_wiki: Zarządzanie wiki
+ permission_move_issues: Przenoszenie zagadnień
+ permission_protect_wiki_pages: Blokowanie stron wiki
+ permission_rename_wiki_pages: Zmiana nazw stron wiki
+ permission_save_queries: Zapisywanie kwerend
+ permission_select_project_modules: Wybieranie modułów projektu
+ permission_view_calendar: Podgląd kalendarza
+ permission_view_changesets: Podgląd zmian
+ permission_view_documents: Podgląd dokumentów
+ permission_view_files: Podgląd plików
+ permission_view_gantt: Podgląd diagramu Gantta
+ permission_view_issue_watchers: Podgląd listy obserwatorów
+ permission_view_messages: Podgląd wiadomości
+ permission_view_time_entries: Podgląd spędzonego czasu
+ permission_view_wiki_edits: Podgląd historii wiki
+ permission_view_wiki_pages: Podgląd wiki
+ project_module_boards: Fora
+ project_module_documents: Dokumenty
+ project_module_files: Pliki
+ project_module_issue_tracking: Śledzenie zagadnień
+ project_module_news: Komunikaty
+ project_module_repository: Repozytorium
+ project_module_time_tracking: Śledzenie czasu
+ project_module_wiki: Wiki
+ setting_activity_days_default: Dni wyświetlane w aktywności projektu
+ setting_app_subtitle: Podtytuł aplikacji
+ setting_app_title: Tytuł aplikacji
+ setting_attachment_max_size: Maks. rozm. załącznika
+ setting_autofetch_changesets: Automatyczne pobieranie zmian
+ setting_autologin: Auto logowanie
+ setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc)
+ setting_commit_fix_keywords: Słowa zmieniające status
+ setting_commit_logs_encoding: Kodowanie komentarzy zatwierdzeń
+ setting_commit_ref_keywords: Słowa tworzące powiązania
+ setting_cross_project_issue_relations: Zezwól na powiązania zagadnień między projektami
+ setting_date_format: Format daty
+ setting_default_language: Domyślny język
+ setting_default_projects_public: Nowe projekty są domyślnie publiczne
+ setting_display_subprojects_issues: Domyślnie pokazuj zagadnienia podprojektów w głównym projekcie
+ setting_emails_footer: Stopka e-mail
+ setting_enabled_scm: Dostępny SCM
+ setting_feeds_limit: Limit danych RSS
+ setting_gravatar_enabled: Używaj ikon użytkowników Gravatar
+ setting_host_name: Nazwa hosta i ścieżka
+ setting_issue_list_default_columns: Domyślne kolumny wiświetlane na liście zagadnień
+ setting_issues_export_limit: Limit eksportu zagadnień
+ setting_login_required: Identyfikacja wymagana
+ setting_mail_from: Adres email wysyłki
+ setting_mail_handler_api_enabled: Uaktywnij usługi sieciowe (WebServices) dla poczty przychodzącej
+ setting_mail_handler_api_key: Klucz API
+ setting_per_page_options: Opcje ilości obiektów na stronie
+ setting_plain_text_mail: tylko tekst (bez HTML)
+ setting_protocol: Protokoł
+ setting_repositories_encodings: Kodowanie repozytoriów
+ setting_self_registration: Samodzielna rejestracja użytkowników
+ setting_sequential_project_identifiers: Generuj sekwencyjne identyfikatory projektów
+ setting_sys_api_enabled: Włączenie WS do zarządzania repozytorium
+ setting_text_formatting: Formatowanie tekstu
+ setting_time_format: Format czasu
+ setting_user_format: Personalny format wyświetlania
+ setting_welcome_text: Tekst powitalny
+ setting_wiki_compression: Kompresja historii Wiki
+ status_active: aktywny
+ status_locked: zablokowany
+ status_registered: zarejestrowany
+ text_are_you_sure: Jesteś pewien ?
+ text_assign_time_entries_to_project: Przypisz logowany czas do projektu
+ text_caracters_maximum: "{{count}} znaków maksymalnie."
+ text_caracters_minimum: "Musi być nie krótsze niż {{count}} znaków."
+ text_comma_separated: Wielokrotne wartości dozwolone (rozdzielone przecinkami).
+ text_default_administrator_account_changed: Zmieniono domyślne hasło administratora
+ text_destroy_time_entries: Usuń zalogowany czas
+ text_destroy_time_entries_question: Zalogowano {{hours}} godzin przy zagadnieniu, które chcesz usunąć. Co chcesz zrobić?
+ text_email_delivery_not_configured: "Dostarczanie poczty elektronicznej nie zostało skonfigurowane, więc powiadamianie jest nieaktywne.\nSkonfiguruj serwer SMTP w config/email.yml a następnie zrestartuj aplikację i uaktywnij to."
+ text_enumeration_category_reassign_to: 'Zmień przypisanie na tą wartość:'
+ text_enumeration_destroy_question: "{{count}} obiektów jest przypisana do tej wartości."
+ text_file_repository_writable: Zapisywalne repozytorium plików
+ text_issue_added: "Zagadnienie {{id}} zostało wprowadzone (by {{author}})."
+ text_issue_category_destroy_assignments: Usuń przydziały kategorii
+ text_issue_category_destroy_question: "Zagadnienia ({{count}}) są przypisane do tej kategorii. Co chcesz uczynić?"
+ text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii
+ text_issue_updated: "Zagadnienie {{id}} zostało zaktualizowane (by {{author}})."
+ text_issues_destroy_confirmation: 'Czy jestes pewien, że chcesz usunąć wskazane zagadnienia?'
+ text_issues_ref_in_commit_messages: Odwołania do zagadnień w komentarzach zatwierdzeń
+ text_length_between: "Długość pomiędzy {{min}} i {{max}} znaków."
+ text_load_default_configuration: Załaduj domyślną konfigurację
+ text_min_max_length_info: 0 oznacza brak restrykcji
+ text_no_configuration_data: "Role użytkowników, typy zagadnień, statusy zagadnień oraz przepływ pracy nie zostały jeszcze skonfigurowane.\nJest wysoce rekomendowane by załadować domyślną konfigurację. Po załadowaniu będzie możliwość edycji tych danych."
+ text_project_destroy_confirmation: Jesteś pewien, że chcesz usunąć ten projekt i wszyskie powiązane dane?
+ text_project_identifier_info: 'Małe litery (a-z), liczby i myślniki dozwolone.<br />Raz zapisany, identyfikator nie może być zmieniony.'
+ text_reassign_time_entries: 'Przepnij zalogowany czas do tego zagadnienia:'
+ text_regexp_info: np. ^[A-Z0-9]+$
+ text_repository_usernames_mapping: "Wybierz lub uaktualnij przyporządkowanie użytkowników Redmine do użytkowników repozytorium.\nUżytkownicy z taką samą nazwą lub adresem email są przyporządkowani automatycznie."
+ text_rmagick_available: RMagick dostępne (opcjonalnie)
+ text_select_mail_notifications: Zaznacz czynności przy których użytkownik powinien być powiadomiony mailem.
+ text_select_project_modules: 'Wybierz moduły do aktywacji w tym projekcie:'
+ text_status_changed_by_changeset: "Zastosowane w zmianach {{value}}."
+ text_subprojects_destroy_warning: "Podprojekt(y): {{value}} zostaną także usunięte."
+ text_tip_task_begin_day: zadanie zaczynające się dzisiaj
+ text_tip_task_begin_end_day: zadanie zaczynające i kończące się dzisiaj
+ text_tip_task_end_day: zadanie kończące się dzisiaj
+ text_tracker_no_workflow: Brak przepływu zefiniowanego dla tego typu zagadnienia
+ text_unallowed_characters: Niedozwolone znaki
+ text_user_mail_option: "W przypadku niezaznaczonych projektów, będziesz otrzymywał powiadomienia tylko na temat zagadnien, które obserwujesz, lub w których bierzesz udział (np. jesteś autorem lub adresatem)."
+ text_user_wrote: "{{value}} napisał:"
+ text_wiki_destroy_confirmation: Jesteś pewien, że chcesz usunąć to wiki i całą jego zawartość ?
+ text_workflow_edit: Zaznacz rolę i typ zagadnienia do edycji przepływu
+
+ label_user_activity: "Aktywność: {{value}}"
+ label_updated_time_by: "Uaktualnione przez {{author}} {{age}} temu"
+ text_diff_truncated: '... Ten plik różnic został przycięty ponieważ jest zbyt długi.'
+ setting_diff_max_lines_displayed: Maksymalna liczba linii różnicy do pokazania
+ text_plugin_assets_writable: Zapisywalny katalog zasobów wtyczek
+ warning_attachments_not_saved: "{{count}} załącznik(ów) nie zostało zapisanych."
+ field_editable: Edytowalne
+ label_display: Wygląd
+ button_create_and_continue: Stwórz i dodaj kolejne
+ text_custom_field_possible_values_info: 'Każda wartość w osobnej linii'
+ setting_repository_log_display_limit: Maksymalna liczba rewizji pokazywanych w logu pliku
+ setting_file_max_size_displayed: Maksymalny rozmiar plików tekstowych zagnieżdżanych w stronie
+ field_watcher: Obserwator
+ setting_openid: Logowanie i rejestracja przy użyciu OpenID
+ field_identity_url: Identyfikator OpenID (URL)
+ label_login_with_open_id_option: albo użyj OpenID
+ field_content: Treść
+ label_descending: Malejąco
+ label_sort: Sortuj
+ label_ascending: Rosnąco
+ label_date_from_to: Od {{start}} do {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: 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: Minimalna długość hasła
+ field_group_by: Grupuj wyniki wg
+ mail_subject_wiki_content_updated: "Strona wiki '{{page}}' została uaktualniona"
+ label_wiki_content_added: Dodano stronę wiki
+ mail_subject_wiki_content_added: "Strona wiki '{{page}}' została dodana"
+ mail_body_wiki_content_added: Strona wiki '{{page}}' została dodana przez {{author}}.
+ label_wiki_content_updated: Uaktualniono stronę wiki
+ mail_body_wiki_content_updated: Strona wiki '{{page}}' została uaktualniona przez {{author}}.
+ permission_add_project: Tworzenie projektu
+ setting_new_project_user_role_id: Role given to a non-admin user who creates a project
+ label_view_all_revisions: Pokaż wszystkie rewizje
+ label_tag: Tag
+ label_branch: Gałąź
+ error_no_tracker_in_project: Projekt nie posiada powiązanych typów zagadnień. Sprawdź ustawienia projektu.
+ error_no_default_issue_status: Nie zdefiniowano domyślnego statusu zagadnień. Sprawdź konfigurację (Przejdź do "Administracja -> Statusy zagadnień).
+ text_journal_changed: "Zmieniono {{label}} z {{old}} na {{new}}"
+ text_journal_set_to: "Ustawiono {{label}} na {{value}}"
+ text_journal_deleted: "Usunięto {{label}} ({{old}})"
+ label_group_plural: Grupy
+ label_group: Grupa
+ label_group_new: Nowa grupa
+ label_time_entry_plural: Spędzony czas
+ text_journal_added: "Dodano {{label}} {{value}}"
+ field_active: Aktywne
+ enumeration_system_activity: Aktywność Systemowa
+ 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
--- /dev/null
+pt-BR:
+ # formatos de data e hora
+ date:
+ formats:
+ default: "%d/%m/%Y"
+ short: "%d de %B"
+ long: "%d de %B de %Y"
+ only_day: "%d"
+
+ day_names: [Domingo, Segunda, Terça, Quarta, Quinta, Sexta, Sábado]
+ abbr_day_names: [Dom, Seg, Ter, Qua, Qui, Sex, Sáb]
+ month_names: [~, Janeiro, Fevereiro, Março, Abril, Maio, Junho, Julho, Agosto, Setembro, Outubro, Novembro, Dezembro]
+ abbr_month_names: [~, Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez]
+ order: [:day,:month,:year]
+
+ time:
+ formats:
+ default: "%A, %d de %B de %Y, %H:%M hs"
+ time: "%H:%M hs"
+ short: "%d/%m, %H:%M hs"
+ long: "%A, %d de %B de %Y, %H:%M hs"
+ only_second: "%S"
+ datetime:
+ formats:
+ default: "%Y-%m-%dT%H:%M:%S%Z"
+ am: ''
+ pm: ''
+
+ # date helper distanci em palavras
+ datetime:
+ distance_in_words:
+ half_a_minute: 'meio minuto'
+ less_than_x_seconds:
+ one: 'menos de 1 segundo'
+ other: 'menos de {{count}} segundos'
+
+ x_seconds:
+ one: '1 segundo'
+ other: '{{count}} segundos'
+
+ less_than_x_minutes:
+ one: 'menos de um minuto'
+ other: 'menos de {{count}} minutos'
+
+ x_minutes:
+ one: '1 minuto'
+ other: '{{count}} minutos'
+
+ about_x_hours:
+ one: 'aproximadamente 1 hora'
+ other: 'aproximadamente {{count}} horas'
+
+ x_days:
+ one: '1 dia'
+ other: '{{count}} dias'
+
+ about_x_months:
+ one: 'aproximadamente 1 mês'
+ other: 'aproximadamente {{count}} meses'
+
+ x_months:
+ one: '1 mês'
+ other: '{{count}} meses'
+
+ about_x_years:
+ one: 'aproximadamente 1 ano'
+ other: 'aproximadamente {{count}} anos'
+
+ over_x_years:
+ one: 'mais de 1 ano'
+ other: 'mais de {{count}} anos'
+
+ # numeros
+ number:
+ format:
+ precision: 3
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: 'R$'
+ precision: 2
+ format: '%u %n'
+ separator: ','
+ delimiter: '.'
+ percentage:
+ format:
+ delimiter: '.'
+ precision:
+ format:
+ delimiter: '.'
+ human:
+ format:
+ precision: 1
+ delimiter: '.'
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+ support:
+ array:
+ sentence_connector: "e"
+ skip_last_comma: true
+
+ # Active Record
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "model não pode ser salvo: 1 erro"
+ other: "model não pode ser salvo: {{count}} erros."
+ body: "Por favor, verifique os seguintes campos:"
+ messages:
+ inclusion: "não está incluso na lista"
+ exclusion: "não está disponível"
+ invalid: "não é válido"
+ confirmation: "não está de acordo com a confirmação"
+ accepted: "precisa ser aceito"
+ empty: "não pode ficar vazio"
+ blank: "não pode ficar vazio"
+ too_long: "é muito longo (máximo: {{count}} caracteres)"
+ too_short: "é muito curto (mínimon: {{count}} caracteres)"
+ wrong_length: "deve ter {{count}} caracteres"
+ taken: "não está disponível"
+ not_a_number: "não é um número"
+ greater_than: "precisa ser maior do que {{count}}"
+ greater_than_or_equal_to: "precisa ser maior ou igual a {{count}}"
+ equal_to: "precisa ser igual a {{count}}"
+ less_than: "precisa ser menor do que {{count}}"
+ less_than_or_equal_to: "precisa ser menor ou igual a {{count}}"
+ odd: "precisa ser ímpar"
+ even: "precisa ser par"
+ greater_than_start_date: "deve ser maior que a data inicial"
+ not_same_project: "não pertence ao mesmo projeto"
+ circular_dependency: "Esta relação geraria uma dependência circular"
+
+ actionview_instancetag_blank_option: Selecione
+
+ general_text_No: 'Não'
+ general_text_Yes: 'Sim'
+ general_text_no: 'não'
+ general_text_yes: 'sim'
+ general_lang_name: 'Português(Brasil)'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Conta atualizada com sucesso.
+ notice_account_invalid_creditentials: Usuário ou senha inválido.
+ notice_account_password_updated: Senha alterada com sucesso.
+ notice_account_wrong_password: Senha inválida.
+ notice_account_register_done: Conta criada com sucesso. Para ativar sua conta, clique no link que lhe foi enviado por e-mail.
+ notice_account_unknown_email: Usuário desconhecido.
+ notice_can_t_change_password: Esta conta utiliza autenticação externa. Não é possível alterar a senha.
+ notice_account_lost_email_sent: Um e-mail com instruções para escolher uma nova senha foi enviado para você.
+ notice_account_activated: Sua conta foi ativada. Você pode acessá-la agora.
+ notice_successful_create: Criado com sucesso.
+ notice_successful_update: Alterado com sucesso.
+ notice_successful_delete: Excluído com sucesso.
+ notice_successful_connection: Conectado com sucesso.
+ notice_file_not_found: A página que você está tentando acessar não existe ou foi excluída.
+ notice_locking_conflict: Os dados foram atualizados por outro usuário.
+ notice_not_authorized: Você não está autorizado a acessar esta página.
+ notice_email_sent: "Um e-mail foi enviado para {{value}}"
+ notice_email_error: "Ocorreu um erro ao enviar o e-mail ({{value}})"
+ notice_feeds_access_key_reseted: Sua chave RSS foi reconfigurada.
+ notice_failed_to_save_issues: "Problema ao salvar {{count}} tarefa(s) de {{total}} selecionadas: {{ids}}."
+ notice_no_issue_selected: "Nenhuma tarefa selecionada! Por favor, marque as tarefas que você deseja editar."
+ notice_account_pending: "Sua conta foi criada e está aguardando aprovação do administrador."
+ notice_default_data_loaded: Configuração padrão carregada com sucesso.
+
+ error_can_t_load_default_data: "A configuração padrão não pode ser carregada: {{value}}"
+ error_scm_not_found: "A entrada e/ou a revisão não existe no repositório."
+ error_scm_command_failed: "Ocorreu um erro ao tentar acessar o repositório: {{value}}"
+ error_scm_annotate: "Esta entrada não existe ou não pode ser anotada."
+ error_issue_not_found_in_project: 'A tarefa não foi encontrada ou não pertence a este projeto'
+ error_no_tracker_in_project: 'Não há um tipo de tarefa associado a este projeto. favor verificar as configurações do projeto.'
+ error_no_default_issue_status: 'A situação padrão para tarefa não está definida. Favor verificar sua configuração (Vá em "Administração -> Situação da tarefa").'
+
+ mail_subject_lost_password: "Sua senha do {{value}}."
+ mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:'
+ mail_subject_register: "Ativação de conta do {{value}}."
+ mail_body_register: 'Para ativar sua conta, clique no link abaixo:'
+ mail_body_account_information_external: "Você pode usar sua conta do {{value}} para entrar."
+ mail_body_account_information: Informações sobre sua conta
+ mail_subject_account_activation_request: "{{value}} - Requisição de ativação de conta"
+ mail_body_account_activation_request: "Um novo usuário ({{value}}) se registrou. A conta está aguardando sua aprovação:"
+ mail_subject_reminder: "{{count}} tarefa(s) com data prevista para os próximos dias"
+ mail_body_reminder: "{{count}} tarefa(s) para você com data prevista para os próximos {{days}} dias:"
+
+ gui_validation_error: 1 erro
+ gui_validation_error_plural: "{{count}} erros"
+
+ field_name: Nome
+ field_description: Descrição
+ field_summary: Resumo
+ field_is_required: Obrigatório
+ field_firstname: Nome
+ field_lastname: Sobrenome
+ field_mail: E-mail
+ field_filename: Arquivo
+ field_filesize: Tamanho
+ field_downloads: Downloads
+ field_author: Autor
+ field_created_on: Criado em
+ field_updated_on: Alterado em
+ field_field_format: Formato
+ field_is_for_all: Para todos os projetos
+ field_possible_values: Possíveis valores
+ field_regexp: Expressão regular
+ field_min_length: Tamanho mínimo
+ field_max_length: Tamanho máximo
+ field_value: Valor
+ field_category: Categoria
+ field_title: Título
+ field_project: Projeto
+ field_issue: Tarefa
+ field_status: Situação
+ field_notes: Notas
+ field_is_closed: Tarefa fechada
+ field_is_default: Situação padrão
+ field_tracker: Tipo
+ field_subject: Título
+ field_due_date: Data prevista
+ field_assigned_to: Atribuído para
+ field_priority: Prioridade
+ field_fixed_version: Versão
+ field_user: Usuário
+ field_role: Cargo
+ field_homepage: Página inicial
+ field_is_public: Público
+ field_parent: Sub-projeto de
+ field_is_in_chlog: Exibir na lista de alterações
+ field_is_in_roadmap: Exibir no planejamento
+ field_login: Usuário
+ field_mail_notification: Notificações por e-mail
+ field_admin: Administrador
+ field_last_login_on: Última conexão
+ field_language: Idioma
+ field_effective_date: Data
+ field_password: Senha
+ field_new_password: Nova senha
+ field_password_confirmation: Confirmação
+ field_version: Versão
+ field_type: Tipo
+ field_host: Servidor
+ field_port: Porta
+ field_account: Conta
+ field_base_dn: DN Base
+ field_attr_login: Atributo para nome de usuário
+ field_attr_firstname: Atributo para nome
+ field_attr_lastname: Atributo para sobrenome
+ field_attr_mail: Atributo para e-mail
+ field_onthefly: Criar usuários dinamicamente ("on-the-fly")
+ field_start_date: Início
+ field_done_ratio: % Terminado
+ field_auth_source: Modo de autenticação
+ field_hide_mail: Ocultar meu e-mail
+ field_comments: Comentário
+ field_url: URL
+ field_start_page: Página inicial
+ field_subproject: Sub-projeto
+ field_hours: Horas
+ field_activity: Atividade
+ field_spent_on: Data
+ field_identifier: Identificador
+ field_is_filter: É um filtro
+ field_issue_to: Tarefa relacionada
+ field_delay: Atraso
+ field_assignable: Tarefas podem ser atribuídas a esta função
+ field_redirect_existing_links: Redirecionar links existentes
+ field_estimated_hours: Tempo estimado
+ field_column_names: Colunas
+ field_time_zone: Fuso-horário
+ field_searchable: Pesquisável
+ field_default_value: Padrão
+ field_comments_sorting: Visualizar comentários
+ field_parent_title: Página pai
+
+ setting_app_title: Título da aplicação
+ setting_app_subtitle: Sub-título da aplicação
+ setting_welcome_text: Texto de boas-vindas
+ setting_default_language: Idioma padrão
+ setting_login_required: Exigir autenticação
+ setting_self_registration: Permitido Auto-registro
+ setting_attachment_max_size: Tamanho máximo do anexo
+ setting_issues_export_limit: Limite de exportação das tarefas
+ setting_mail_from: E-mail enviado de
+ setting_bcc_recipients: Destinatários com cópia oculta (cco)
+ setting_host_name: Servidor
+ setting_text_formatting: Formato do texto
+ setting_wiki_compression: Compactação de histórico do Wiki
+ setting_feeds_limit: Limite do Feed
+ setting_default_projects_public: Novos projetos são públicos por padrão
+ setting_autofetch_changesets: Auto-obter commits
+ setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
+ setting_commit_ref_keywords: Palavras de referência
+ setting_commit_fix_keywords: Palavras de fechamento
+ setting_autologin: Auto-login
+ setting_date_format: Formato da data
+ setting_time_format: Formato de hora
+ setting_cross_project_issue_relations: Permitir relacionar tarefas entre projetos
+ setting_issue_list_default_columns: Colunas padrão visíveis na lista de tarefas
+ setting_repositories_encodings: Codificação dos repositórios
+ setting_commit_logs_encoding: Codificação das mensagens de commit
+ setting_emails_footer: Rodapé dos e-mails
+ setting_protocol: Protocolo
+ setting_per_page_options: Opções de itens por página
+ setting_user_format: Formato de visualização dos usuários
+ setting_activity_days_default: Dias visualizados na atividade do projeto
+ setting_display_subprojects_issues: Visualizar tarefas dos subprojetos nos projetos principais por padrão
+ setting_enabled_scm: Habilitar SCM
+ setting_mail_handler_api_enabled: Habilitar WS para e-mails de entrada
+ setting_mail_handler_api_key: Chave de API
+ setting_sequential_project_identifiers: Gerar identificadores de projeto sequenciais
+
+ project_module_issue_tracking: Gerenciamento de Tarefas
+ project_module_time_tracking: Gerenciamento de tempo
+ project_module_news: Notícias
+ project_module_documents: Documentos
+ project_module_files: Arquivos
+ project_module_wiki: Wiki
+ project_module_repository: Repositório
+ project_module_boards: Fóruns
+
+ label_user: Usuário
+ label_user_plural: Usuários
+ label_user_new: Novo usuário
+ label_project: Projeto
+ label_project_new: Novo projeto
+ label_project_plural: Projetos
+ label_x_projects:
+ zero: nenhum projeto
+ one: 1 projeto
+ other: "{{count}} projetos"
+ label_project_all: Todos os projetos
+ label_project_latest: Últimos projetos
+ label_issue: Tarefa
+ label_issue_new: Nova tarefa
+ label_issue_plural: Tarefas
+ label_issue_view_all: Ver todas as tarefas
+ label_issues_by: "Tarefas por {{value}}"
+ label_issue_added: Tarefa adicionada
+ label_issue_updated: Tarefa atualizada
+ label_document: Documento
+ label_document_new: Novo documento
+ label_document_plural: Documentos
+ label_document_added: Documento adicionado
+ label_role: Cargo
+ label_role_plural: Cargos
+ label_role_new: Novo cargo
+ label_role_and_permissions: Cargos e permissões
+ label_member: Membro
+ label_member_new: Novo membro
+ label_member_plural: Membros
+ label_tracker: Tipo de tarefa
+ label_tracker_plural: Tipos de tarefas
+ label_tracker_new: Novo tipo
+ label_workflow: Fluxo de trabalho
+ label_issue_status: Situação da tarefa
+ label_issue_status_plural: Situação das tarefas
+ label_issue_status_new: Nova situação
+ label_issue_category: Categoria da tarefa
+ label_issue_category_plural: Categorias das tarefas
+ label_issue_category_new: Nova categoria
+ label_custom_field: Campo personalizado
+ label_custom_field_plural: Campos personalizados
+ label_custom_field_new: Novo campo personalizado
+ label_enumerations: 'Tipos & Categorias'
+ label_enumeration_new: Novo
+ label_information: Informação
+ label_information_plural: Informações
+ label_please_login: Efetue o login
+ label_register: Cadastre-se
+ label_password_lost: Perdi minha senha
+ label_home: Página inicial
+ label_my_page: Minha página
+ label_my_account: Minha conta
+ label_my_projects: Meus projetos
+ label_administration: Administração
+ label_login: Entrar
+ label_logout: Sair
+ label_help: Ajuda
+ label_reported_issues: Tarefas reportadas
+ label_assigned_to_me_issues: Minhas tarefas
+ label_last_login: Última conexão
+ label_registered_on: Registrado em
+ label_activity: Atividade
+ label_overall_activity: Atividades gerais
+ label_new: Novo
+ label_logged_as: "Acessando como:"
+ label_environment: Ambiente
+ label_authentication: Autenticação
+ label_auth_source: Modo de autenticação
+ label_auth_source_new: Novo modo de autenticação
+ label_auth_source_plural: Modos de autenticação
+ label_subproject_plural: Sub-projetos
+ label_and_its_subprojects: "{{value}} e seus sub-projetos"
+ label_min_max_length: Tamanho mín-máx
+ label_list: Lista
+ label_date: Data
+ label_integer: Inteiro
+ label_float: Decimal
+ label_boolean: Boleano
+ label_string: Texto
+ label_text: Texto longo
+ label_attribute: Atributo
+ label_attribute_plural: Atributos
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Nenhuma informação disponível
+ label_change_status: Alterar situação
+ label_history: Histórico
+ label_attachment: Arquivo
+ label_attachment_new: Novo arquivo
+ label_attachment_delete: Excluir arquivo
+ label_attachment_plural: Arquivos
+ label_file_added: Arquivo adicionado
+ label_report: Relatório
+ label_report_plural: Relatório
+ label_news: Notícia
+ label_news_new: Adicionar notícia
+ label_news_plural: Notícias
+ label_news_latest: Últimas notícias
+ label_news_view_all: Ver todas as notícias
+ label_news_added: Notícia adicionada
+ label_change_log: Registro de alterações
+ label_settings: Configurações
+ label_overview: Visão geral
+ label_version: Versão
+ label_version_new: Nova versão
+ label_version_plural: Versões
+ label_confirmation: Confirmação
+ label_export_to: Exportar para
+ label_read: Ler...
+ label_public_projects: Projetos públicos
+ label_open_issues: Aberta
+ label_open_issues_plural: Abertas
+ label_closed_issues: Fechada
+ label_closed_issues_plural: Fechadas
+ label_x_open_issues_abbr_on_total:
+ zero: 0 aberta / {{total}}
+ one: 1 aberta / {{total}}
+ other: "{{count}} abertas / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 aberta
+ one: 1 aberta
+ other: "{{count}} abertas"
+ label_x_closed_issues_abbr:
+ zero: 0 fechada
+ one: 1 fechada
+ other: "{{count}} fechadas"
+ label_total: Total
+ label_permissions: Permissões
+ label_current_status: Situação atual
+ label_new_statuses_allowed: Nova situação permitida
+ label_all: todos
+ label_none: nenhum
+ label_nobody: ninguém
+ label_next: Próximo
+ label_previous: Anterior
+ label_used_by: Usado por
+ label_details: Detalhes
+ label_add_note: Adicionar nota
+ label_per_page: Por página
+ label_calendar: Calendário
+ label_months_from: meses a partir de
+ label_gantt: Gantt
+ label_internal: Interno
+ label_last_changes: "últimas {{count}} alterações"
+ label_change_view_all: Mostrar todas as alterações
+ label_personalize_page: Personalizar esta página
+ label_comment: Comentário
+ label_comment_plural: Comentários
+ label_x_comments:
+ zero: nenhum comentário
+ one: 1 comentário
+ other: "{{count}} comentários"
+ label_comment_add: Adicionar comentário
+ label_comment_added: Comentário adicionado
+ label_comment_delete: Excluir comentário
+ label_query: Consulta personalizada
+ label_query_plural: Consultas personalizadas
+ label_query_new: Nova consulta
+ label_filter_add: Adicionar filtro
+ label_filter_plural: Filtros
+ label_equals: igual a
+ label_not_equals: diferente de
+ label_in_less_than: maior que
+ label_in_more_than: menor que
+ label_in: em
+ label_today: hoje
+ label_all_time: tudo
+ label_yesterday: ontem
+ label_this_week: esta semana
+ label_last_week: última semana
+ label_last_n_days: "últimos {{count}} dias"
+ label_this_month: este mês
+ label_last_month: último mês
+ label_this_year: este ano
+ label_date_range: Período
+ label_less_than_ago: menos de
+ label_more_than_ago: mais de
+ label_ago: dias atrás
+ label_contains: contém
+ label_not_contains: não contém
+ label_day_plural: dias
+ label_repository: Repositório
+ label_repository_plural: Repositórios
+ label_browse: Procurar
+ label_modification: "{{count}} alteração"
+ label_modification_plural: "{{count}} alterações"
+ label_revision: Revisão
+ label_revision_plural: Revisões
+ label_associated_revisions: Revisões associadas
+ label_added: adicionada
+ label_modified: alterada
+ label_deleted: excluída
+ label_latest_revision: Última revisão
+ label_latest_revision_plural: Últimas revisões
+ label_view_revisions: Ver revisões
+ label_max_size: Tamanho máximo
+ label_sort_highest: Mover para o início
+ label_sort_higher: Mover para cima
+ label_sort_lower: Mover para baixo
+ label_sort_lowest: Mover para o fim
+ label_roadmap: Planejamento
+ label_roadmap_due_in: "Previsto para {{value}}"
+ label_roadmap_overdue: "{{value}} atrasado"
+ label_roadmap_no_issues: Sem tarefas para esta versão
+ label_search: Busca
+ label_result_plural: Resultados
+ label_all_words: Todas as palavras
+ label_wiki: Wiki
+ label_wiki_edit: Editar Wiki
+ label_wiki_edit_plural: Edições Wiki
+ label_wiki_page: Página Wiki
+ label_wiki_page_plural: páginas Wiki
+ label_index_by_title: Índice por título
+ label_index_by_date: Índice por data
+ label_current_version: Versão atual
+ label_preview: Pré-visualizar
+ label_feed_plural: Feeds
+ label_changes_details: Detalhes de todas as alterações
+ label_issue_tracking: Tarefas
+ label_spent_time: Tempo gasto
+ label_f_hour: "{{value}} hora"
+ label_f_hour_plural: "{{value}} horas"
+ label_time_tracking: Controle de horas
+ label_change_plural: Alterações
+ label_statistics: Estatísticas
+ label_commits_per_month: Commits por mês
+ label_commits_per_author: Commits por autor
+ label_view_diff: Ver diferenças
+ label_diff_inline: inline
+ label_diff_side_by_side: lado a lado
+ label_options: Opções
+ label_copy_workflow_from: Copiar fluxo de trabalho de
+ label_permissions_report: Relatório de permissões
+ label_watched_issues: Tarefas monitoradas
+ label_related_issues: Tarefas relacionadas
+ label_applied_status: Situação alterada
+ label_loading: Carregando...
+ label_relation_new: Nova relação
+ label_relation_delete: Excluir relação
+ label_relates_to: relacionado a
+ label_duplicates: duplica
+ label_duplicated_by: duplicado por
+ label_blocks: bloqueia
+ label_blocked_by: bloqueado por
+ label_precedes: precede
+ label_follows: segue
+ label_end_to_start: fim para o início
+ label_end_to_end: fim para fim
+ label_start_to_start: início para início
+ label_start_to_end: início para fim
+ label_stay_logged_in: Permanecer logado
+ label_disabled: desabilitado
+ label_show_completed_versions: Exibir versões completas
+ label_me: mim
+ label_board: Fórum
+ label_board_new: Novo fórum
+ label_board_plural: Fóruns
+ label_topic_plural: Tópicos
+ label_message_plural: Mensagens
+ label_message_last: Última mensagem
+ label_message_new: Nova mensagem
+ label_message_posted: Mensagem enviada
+ label_reply_plural: Respostas
+ label_send_information: Enviar informação da nova conta para o usuário
+ label_year: Ano
+ label_month: Mês
+ label_week: Semana
+ label_date_from: De
+ label_date_to: Para
+ label_language_based: Com base no idioma do usuário
+ label_sort_by: "Ordenar por {{value}}"
+ label_send_test_email: Enviar um e-mail de teste
+ label_feeds_access_key_created_on: "chave de acesso RSS criada {{value}} atrás"
+ label_module_plural: Módulos
+ label_added_time_by: "Adicionado por {{author}} {{age}} atrás"
+ label_updated_time: "Atualizado {{value}} atrás"
+ label_jump_to_a_project: Ir para o projeto...
+ label_file_plural: Arquivos
+ label_changeset_plural: Changesets
+ label_default_columns: Colunas padrão
+ label_no_change_option: (Sem alteração)
+ label_bulk_edit_selected_issues: Edição em massa das tarefas selecionados.
+ label_theme: Tema
+ label_default: Padrão
+ label_search_titles_only: Pesquisar somente títulos
+ label_user_mail_option_all: "Para qualquer evento em todos os meus projetos"
+ label_user_mail_option_selected: "Para qualquer evento somente no(s) projeto(s) selecionado(s)..."
+ label_user_mail_option_none: "Somente tarefas que eu acompanho ou estou envolvido"
+ label_user_mail_no_self_notified: "Eu não quero ser notificado de minhas próprias modificações"
+ label_registration_activation_by_email: ativação de conta por e-mail
+ label_registration_manual_activation: ativação manual de conta
+ label_registration_automatic_activation: ativação automática de conta
+ label_display_per_page: "Por página: {{value}}"
+ label_age: Idade
+ label_change_properties: Alterar propriedades
+ label_general: Geral
+ label_more: Mais
+ label_scm: 'Controle de versão:'
+ label_plugins: Plugins
+ label_ldap_authentication: Autenticação LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Descrição opcional
+ label_add_another_file: Adicionar outro arquivo
+ label_preferences: Preferências
+ label_chronological_order: Em ordem cronológica
+ label_reverse_chronological_order: Em ordem cronológica inversa
+ label_planning: Planejamento
+ label_incoming_emails: E-mail de entrada
+ label_generate_key: Gerar uma chave
+ label_issue_watchers: Monitorando
+
+ button_login: Entrar
+ button_submit: Enviar
+ button_save: Salvar
+ button_check_all: Marcar todos
+ button_uncheck_all: Desmarcar todos
+ button_delete: Excluir
+ button_create: Criar
+ button_test: Testar
+ button_edit: Editar
+ button_add: Adicionar
+ button_change: Alterar
+ button_apply: Aplicar
+ button_clear: Limpar
+ button_lock: Bloquear
+ button_unlock: Desbloquear
+ button_download: Baixar
+ button_list: Listar
+ button_view: Ver
+ button_move: Mover
+ button_back: Voltar
+ button_cancel: Cancelar
+ button_activate: Ativar
+ button_sort: Ordenar
+ button_log_time: Tempo de trabalho
+ button_rollback: Voltar para esta versão
+ button_watch: Monitorar
+ button_unwatch: Parar de Monitorar
+ button_reply: Responder
+ button_archive: Arquivar
+ button_unarchive: Desarquivar
+ button_reset: Redefinir
+ button_rename: Renomear
+ button_change_password: Alterar senha
+ button_copy: Copiar
+ button_annotate: Anotar
+ button_update: Atualizar
+ button_configure: Configurar
+ button_quote: Responder
+
+ status_active: ativo
+ status_registered: registrado
+ status_locked: bloqueado
+
+ text_select_mail_notifications: Selecionar ações para ser enviado uma notificação por e-mail
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 = sem restrição
+ text_project_destroy_confirmation: Você tem certeza que deseja excluir este projeto e todos os dados relacionados?
+ text_subprojects_destroy_warning: "Seu(s) subprojeto(s): {{value}} também serão excluídos."
+ text_workflow_edit: Selecione um cargo e um tipo de tarefa para editar o fluxo de trabalho
+ text_are_you_sure: Você tem certeza?
+ text_tip_task_begin_day: tarefa inicia neste dia
+ text_tip_task_end_day: tarefa termina neste dia
+ text_tip_task_begin_end_day: tarefa inicia e termina neste dia
+ text_project_identifier_info: 'Letras minúsculas (a-z), números e hífens permitidos.<br />Uma vez salvo, o identificador não poderá ser alterado.'
+ text_caracters_maximum: "máximo {{count}} caracteres"
+ text_caracters_minimum: "deve ter ao menos {{count}} caracteres."
+ text_length_between: "deve ter entre {{min}} e {{max}} caracteres."
+ text_tracker_no_workflow: Sem fluxo de trabalho definido para este tipo.
+ text_unallowed_characters: Caracteres não permitidos
+ text_comma_separated: Múltiplos valores são permitidos (separados por vírgula).
+ text_issues_ref_in_commit_messages: Referenciar tarefas nas mensagens de commit
+ text_issue_added: "Tarefa {{id}} incluída (por {{author}})."
+ text_issue_updated: "Tarefa {{id}} alterada (por {{author}})."
+ text_wiki_destroy_confirmation: Você tem certeza que deseja excluir este wiki e TODO o seu conteúdo?
+ text_issue_category_destroy_question: "Algumas tarefas ({{count}}) estão atribuídas a esta categoria. O que você deseja fazer?"
+ text_issue_category_destroy_assignments: Remover atribuições da categoria
+ text_issue_category_reassign_to: Redefinir tarefas para esta categoria
+ text_user_mail_option: "Para projetos (não selecionados), você somente receberá notificações sobre o que você monitora ou está envolvido (ex. tarefas das quais você é o autor ou que estão atribuídas a você)"
+ text_no_configuration_data: "Os Papéis, tipos de tarefas, situação de tarefas e fluxos de trabalho não foram configurados ainda.\nÉ altamente recomendado carregar as configurações padrão. Você poderá modificar estas configurações assim que carregadas."
+ text_load_default_configuration: Carregar a configuração padrão
+ text_status_changed_by_changeset: "Aplicado no changeset {{value}}."
+ text_issues_destroy_confirmation: 'Você tem certeza que deseja excluir a(s) tarefa(s) selecionada(s)?'
+ text_select_project_modules: 'Selecione módulos para habilitar para este projeto:'
+ text_default_administrator_account_changed: Conta padrão do administrador alterada
+ text_file_repository_writable: Repositório com permissão de escrita
+ text_rmagick_available: RMagick disponível (opcional)
+ text_destroy_time_entries_question: "{{hours}} horas de trabalho foram registradas nas tarefas que você está excluindo. O que você deseja fazer?"
+ text_destroy_time_entries: Excluir horas de trabalho
+ text_assign_time_entries_to_project: Atribuir estas horas de trabalho para outro projeto
+ text_reassign_time_entries: 'Atribuir horas reportadas para esta tarefa:'
+ text_user_wrote: "{{value}} escreveu:"
+ text_enumeration_destroy_question: "{{count}} objetos estão atribuídos a este valor."
+ text_enumeration_category_reassign_to: 'Reatribuí-los ao valor:'
+ text_email_delivery_not_configured: "O envio de e-mail não está configurado, e as notificações estão inativas.\nConfigure seu servidor SMTP no arquivo config/email.yml e reinicie a aplicação para ativá-las."
+
+ default_role_manager: Gerente
+ default_role_developper: Desenvolvedor
+ default_role_reporter: Informante
+ default_tracker_bug: Problema
+ default_tracker_feature: Funcionalidade
+ default_tracker_support: Suporte
+ default_issue_status_new: Nova
+ default_issue_status_in_progress: Em andamento
+ default_issue_status_resolved: Resolvida
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Fechada
+ default_issue_status_rejected: Rejeitada
+ default_doc_category_user: Documentação do usuário
+ default_doc_category_tech: Documentação técnica
+ default_priority_low: Baixa
+ default_priority_normal: Normal
+ default_priority_high: Alta
+ default_priority_urgent: Urgente
+ default_priority_immediate: Imediata
+ default_activity_design: Design
+ default_activity_development: Desenvolvimento
+
+ enumeration_issue_priorities: Prioridade das tarefas
+ enumeration_doc_categories: Categorias de documento
+ enumeration_activities: Atividades (time tracking)
+ notice_unable_delete_version: Não foi possível excluir a versão
+ label_renamed: renomeado
+ label_copied: copiado
+ setting_plain_text_mail: apenas texto sem formatação (sem HTML)
+ permission_view_files: Ver arquivos
+ permission_edit_issues: Editar tarefas
+ permission_edit_own_time_entries: Editar o próprio tempo de trabalho
+ permission_manage_public_queries: Gerenciar consultas publicas
+ permission_add_issues: Adicionar tarefas
+ permission_log_time: Adicionar tempo gasto
+ permission_view_changesets: Ver changesets
+ permission_view_time_entries: Ver tempo gasto
+ permission_manage_versions: Gerenciar versões
+ permission_manage_wiki: Gerenciar wiki
+ permission_manage_categories: Gerenciar categorias de tarefas
+ permission_protect_wiki_pages: Proteger páginas wiki
+ permission_comment_news: Comentar notícias
+ permission_delete_messages: Excluir mensagens
+ permission_select_project_modules: Selecionar módulos de projeto
+ permission_manage_documents: Gerenciar documentos
+ permission_edit_wiki_pages: Editar páginas wiki
+ permission_add_issue_watchers: Adicionar monitores
+ permission_view_gantt: Ver gráfico gantt
+ permission_move_issues: Mover tarefas
+ permission_manage_issue_relations: Gerenciar relacionamentos de tarefas
+ permission_delete_wiki_pages: Excluir páginas wiki
+ permission_manage_boards: Gerenciar fóruns
+ permission_delete_wiki_pages_attachments: Excluir anexos
+ permission_view_wiki_edits: Ver histórico do wiki
+ permission_add_messages: Postar mensagens
+ permission_view_messages: Ver mensagens
+ permission_manage_files: Gerenciar arquivos
+ permission_edit_issue_notes: Editar notas
+ permission_manage_news: Gerenciar notícias
+ permission_view_calendar: Ver calendário
+ permission_manage_members: Gerenciar membros
+ permission_edit_messages: Editar mensagens
+ permission_delete_issues: Excluir tarefas
+ permission_view_issue_watchers: Ver lista de monitores
+ permission_manage_repository: Gerenciar repositório
+ permission_commit_access: Acesso de commit
+ permission_browse_repository: Pesquisar repositório
+ permission_view_documents: Ver documentos
+ permission_edit_project: Editar projeto
+ permission_add_issue_notes: Adicionar notas
+ permission_save_queries: Salvar consultas
+ permission_view_wiki_pages: Ver wiki
+ permission_rename_wiki_pages: Renomear páginas wiki
+ permission_edit_time_entries: Editar tempo gasto
+ permission_edit_own_issue_notes: Editar suas próprias notas
+ setting_gravatar_enabled: Usar ícones do Gravatar
+ label_example: Exemplo
+ text_repository_usernames_mapping: "Seleciona ou atualiza os usuários do Redmine mapeando para cada usuário encontrado no log do repositório.\nUsuários com o mesmo login ou e-mail no Redmine e no repositório serão mapeados automaticamente."
+ permission_edit_own_messages: Editar próprias mensagens
+ permission_delete_own_messages: Excluir próprias mensagens
+ label_user_activity: "Atividade de {{value}}"
+ label_updated_time_by: "Atualizado por {{author}} há {{age}}"
+ text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser exibido.'
+ setting_diff_max_lines_displayed: Número máximo de linhas exibidas no diff
+ text_plugin_assets_writable: Diretório de plugins gravável
+ warning_attachments_not_saved: "{{count}} arquivo(s) não puderam ser salvo(s)."
+ button_create_and_continue: Criar e continuar
+ text_custom_field_possible_values_info: 'Uma linha para cada valor'
+ label_display: Exibição
+ field_editable: Editável
+ setting_repository_log_display_limit: Número máximo de revisões exibidas no arquivo de log
+ setting_file_max_size_displayed: Tamanho máximo dos arquivos textos exibidos inline
+ field_watcher: Observador
+ setting_openid: Permitir Login e Registro via OpenID
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: ou use o OpenID
+ field_content: Conteúdo
+ label_descending: Descendente
+ label_sort: Ordenar
+ label_ascending: Ascendente
+ label_date_from_to: De {{start}} até {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Esta página tem {{descendants}} página(s) filha(s) e descendente(s). O que você quer fazer?
+ text_wiki_page_reassign_children: Reatribuir páginas filhas para esta página pai
+ text_wiki_page_nullify_children: Manter as páginas filhas como páginas raízes
+ text_wiki_page_destroy_children: Excluir páginas filhas e todas suas descendentes
+ setting_password_min_length: Comprimento mínimo para senhas
+ field_group_by: Agrupar por
+ mail_subject_wiki_content_updated: "A página wiki '{{page}}' foi atualizada"
+ label_wiki_content_added: Página wiki adicionada
+ mail_subject_wiki_content_added: "A página wiki '{{page}}' foi adicionada"
+ mail_body_wiki_content_added: A página wiki '{{page}}' foi adicionada por {{author}}.
+ label_wiki_content_updated: Página wiki atualizada
+ mail_body_wiki_content_updated: A página wiki '{{page}}' foi atualizada por {{author}}.
+ permission_add_project: Criar projeto
+ setting_new_project_user_role_id: Cargo dado a um usuário não administrador que crie um projeto
+ label_view_all_revisions: Ver todas as revisões
+ label_tag: Etiqueta
+ label_branch: Ramo
+ text_journal_changed: "{{label}} alterado de {{old}} para {{new}}"
+ text_journal_set_to: "{{label}} ajustado para {{value}}"
+ text_journal_deleted: "{{label}} excluído ({{old}})"
+ label_group_plural: Grupos
+ label_group: Grupo
+ label_group_new: Novo grupo
+ label_time_entry_plural: Tempos gastos
+ text_journal_added: "{{label}} {{value}} adicionado"
+ field_active: Ativo
+ enumeration_system_activity: Atividade do sistema
+ permission_delete_issue_watchers: Excluir observadores
+ version_status_closed: fechado
+ version_status_locked: travado
+ version_status_open: aberto
+ error_can_not_reopen_issue_on_closed_version: Uma tarefa atribuída a uma versão fechada não pode ser reaberta
+ label_user_anonymous: Anônimo
+ button_move_and_follow: Mover e seguir
+ setting_default_projects_modules: Módulos habilitados por padrão para novos projetos
+ setting_gravatar_default: Imagem Gravatar padrão
--- /dev/null
+# Portuguese localization for Ruby on Rails
+# by Ricardo Otero <oterosantos@gmail.com>
+pt:
+ support:
+ array:
+ sentence_connector: "e"
+ skip_last_comma: true
+
+ date:
+ formats:
+ default: "%d/%m/%Y"
+ short: "%d de %B"
+ long: "%d de %B de %Y"
+ only_day: "%d"
+ day_names: [Domingo, Segunda, Terça, Quarta, Quinta, Sexta, Sábado]
+ abbr_day_names: [Dom, Seg, Ter, Qua, Qui, Sex, Sáb]
+ month_names: [~, Janeiro, Fevereiro, Março, Abril, Maio, Junho, Julho, Agosto, Setembro, Outubro, Novembro, Dezembro]
+ abbr_month_names: [~, Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez]
+ order: [:day, :month, :year]
+
+ time:
+ formats:
+ default: "%A, %d de %B de %Y, %H:%Mh"
+ time: "%H:%M"
+ short: "%d/%m, %H:%M hs"
+ long: "%A, %d de %B de %Y, %H:%Mh"
+ am: ''
+ pm: ''
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "meio minuto"
+ less_than_x_seconds:
+ one: "menos de 1 segundo"
+ other: "menos de {{count}} segundos"
+ x_seconds:
+ one: "1 segundo"
+ other: "{{count}} segundos"
+ less_than_x_minutes:
+ one: "menos de um minuto"
+ other: "menos de {{count}} minutos"
+ x_minutes:
+ one: "1 minuto"
+ other: "{{count}} minutos"
+ about_x_hours:
+ one: "aproximadamente 1 hora"
+ other: "aproximadamente {{count}} horas"
+ x_days:
+ one: "1 dia"
+ other: "{{count}} dias"
+ about_x_months:
+ one: "aproximadamente 1 mês"
+ other: "aproximadamente {{count}} meses"
+ x_months:
+ one: "1 mês"
+ other: "{{count}} meses"
+ about_x_years:
+ one: "aproximadamente 1 ano"
+ other: "aproximadamente {{count}} anos"
+ over_x_years:
+ one: "mais de 1 ano"
+ other: "mais de {{count}} anos"
+
+ number:
+ format:
+ precision: 3
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: '€'
+ precision: 2
+ format: "%u %n"
+ separator: ','
+ delimiter: '.'
+ percentage:
+ format:
+ delimiter: ''
+ precision:
+ format:
+ delimiter: ''
+ human:
+ format:
+ precision: 1
+ delimiter: ''
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Não foi possível guardar {{model}}: 1 erro"
+ other: "Não foi possível guardar {{model}}: {{count}} erros"
+ body: "Por favor, verifique os seguintes campos:"
+ messages:
+ inclusion: "não está incluído na lista"
+ exclusion: "não está disponível"
+ invalid: "não é válido"
+ confirmation: "não está de acordo com a confirmação"
+ accepted: "precisa de ser aceite"
+ empty: "não pode estar em branco"
+ blank: "não pode estar em branco"
+ too_long: "tem demasiados caracteres (máximo: {{count}} caracteres)"
+ too_short: "tem poucos caracteres (mínimo: {{count}} caracteres)"
+ wrong_length: "não é do tamanho correcto (necessita de ter {{count}} caracteres)"
+ taken: "não está disponível"
+ not_a_number: "não é um número"
+ greater_than: "tem de ser maior do que {{count}}"
+ greater_than_or_equal_to: "tem de ser maior ou igual a {{count}}"
+ equal_to: "tem de ser igual a {{count}}"
+ less_than: "tem de ser menor do que {{count}}"
+ less_than_or_equal_to: "tem de ser menor ou igual a {{count}}"
+ odd: "tem de ser ímpar"
+ even: "tem de ser par"
+ greater_than_start_date: "deve ser maior que a data inicial"
+ not_same_project: "não pertence ao mesmo projecto"
+ circular_dependency: "Esta relação iria criar uma dependência circular"
+
+ ## Translated by: Pedro Araújo <phcrva19@hotmail.com>
+ actionview_instancetag_blank_option: Seleccione
+
+ general_text_No: 'Não'
+ general_text_Yes: 'Sim'
+ general_text_no: 'não'
+ general_text_yes: 'sim'
+ general_lang_name: 'Português'
+ general_csv_separator: ';'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: ISO-8859-15
+ general_pdf_encoding: ISO-8859-15
+ general_first_day_of_week: '1'
+
+ notice_account_updated: A conta foi actualizada com sucesso.
+ notice_account_invalid_creditentials: Utilizador ou palavra-chave inválidos.
+ notice_account_password_updated: A palavra-chave foi alterada com sucesso.
+ notice_account_wrong_password: Palavra-chave errada.
+ notice_account_register_done: A conta foi criada com sucesso.
+ notice_account_unknown_email: Utilizador desconhecido.
+ notice_can_t_change_password: Esta conta utiliza uma fonte de autenticação externa. Não é possível alterar a palavra-chave.
+ notice_account_lost_email_sent: Foi-lhe enviado um e-mail com as instruções para escolher uma nova palavra-chave.
+ notice_account_activated: A sua conta foi activada. Já pode autenticar-se.
+ notice_successful_create: Criado com sucesso.
+ notice_successful_update: Alterado com sucesso.
+ notice_successful_delete: Apagado com sucesso.
+ notice_successful_connection: Ligado com sucesso.
+ notice_file_not_found: A página que está a tentar aceder não existe ou foi removida.
+ notice_locking_conflict: Os dados foram actualizados por outro utilizador.
+ notice_not_authorized: Não está autorizado a visualizar esta página.
+ notice_email_sent: "Foi enviado um e-mail para {{value}}"
+ notice_email_error: "Ocorreu um erro ao enviar o e-mail ({{value}})"
+ notice_feeds_access_key_reseted: A sua chave de RSS foi inicializada.
+ notice_failed_to_save_issues: "Não foi possível guardar {{count}} tarefa(s) das {{total}} seleccionadas: {{ids}}."
+ notice_no_issue_selected: "Nenhuma tarefa seleccionada! Por favor, seleccione as tarefas que quer editar."
+ notice_account_pending: "A sua conta foi criada e está agora à espera de aprovação do administrador."
+ notice_default_data_loaded: Configuração padrão carregada com sucesso.
+ notice_unable_delete_version: Não foi possível apagar a versão.
+
+ error_can_t_load_default_data: "Não foi possível carregar a configuração padrão: {{value}}"
+ error_scm_not_found: "A entrada ou revisão não foi encontrada no repositório."
+ error_scm_command_failed: "Ocorreu um erro ao tentar aceder ao repositório: {{value}}"
+ error_scm_annotate: "A entrada não existe ou não pode ser anotada."
+ error_issue_not_found_in_project: 'A tarefa não foi encontrada ou não pertence a este projecto.'
+
+ mail_subject_lost_password: "Palavra-chave de {{value}}"
+ mail_body_lost_password: 'Para mudar a sua palavra-chave, clique no link abaixo:'
+ mail_subject_register: "Activação de conta de {{value}}"
+ mail_body_register: 'Para activar a sua conta, clique no link abaixo:'
+ mail_body_account_information_external: "Pode utilizar a conta {{value}} para autenticar-se."
+ mail_body_account_information: Informação da sua conta
+ mail_subject_account_activation_request: "Pedido de activação da conta {{value}}"
+ mail_body_account_activation_request: "Um novo utilizador ({{value}}) registou-se. A sua conta está à espera de aprovação:"
+ mail_subject_reminder: "{{count}} tarefa(s) para entregar nos próximos dias"
+ mail_body_reminder: "{{count}} tarefa(s) que estão atribuídas a si estão agendadas para estarem completas nos próximos {{days}} dias:"
+
+ gui_validation_error: 1 erro
+ gui_validation_error_plural: "{{count}} erros"
+
+ field_name: Nome
+ field_description: Descrição
+ field_summary: Sumário
+ field_is_required: Obrigatório
+ field_firstname: Nome
+ field_lastname: Apelido
+ field_mail: E-mail
+ field_filename: Ficheiro
+ field_filesize: Tamanho
+ field_downloads: Downloads
+ field_author: Autor
+ field_created_on: Criado
+ field_updated_on: Alterado
+ field_field_format: Formato
+ field_is_for_all: Para todos os projectos
+ field_possible_values: Valores possíveis
+ field_regexp: Expressão regular
+ field_min_length: Tamanho mínimo
+ field_max_length: Tamanho máximo
+ field_value: Valor
+ field_category: Categoria
+ field_title: Título
+ field_project: Projecto
+ field_issue: Tarefa
+ field_status: Estado
+ field_notes: Notas
+ field_is_closed: Tarefa fechada
+ field_is_default: Valor por omissão
+ field_tracker: Tipo
+ field_subject: Assunto
+ field_due_date: Data final
+ field_assigned_to: Atribuído a
+ field_priority: Prioridade
+ field_fixed_version: Versão
+ field_user: Utilizador
+ field_role: Papel
+ field_homepage: Página
+ field_is_public: Público
+ field_parent: Sub-projecto de
+ field_is_in_chlog: Tarefas mostradas no changelog
+ field_is_in_roadmap: Tarefas mostradas no roadmap
+ field_login: Nome de utilizador
+ field_mail_notification: Notificações por e-mail
+ field_admin: Administrador
+ field_last_login_on: Última visita
+ field_language: Língua
+ field_effective_date: Data
+ field_password: Palavra-chave
+ field_new_password: Nova palavra-chave
+ field_password_confirmation: Confirmação
+ field_version: Versão
+ field_type: Tipo
+ field_host: Servidor
+ field_port: Porta
+ field_account: Conta
+ field_base_dn: Base DN
+ field_attr_login: Atributo utilizador
+ field_attr_firstname: Atributo nome próprio
+ field_attr_lastname: Atributo último nome
+ field_attr_mail: Atributo e-mail
+ field_onthefly: Criação de utilizadores na hora
+ field_start_date: Início
+ field_done_ratio: % Completo
+ field_auth_source: Modo de autenticação
+ field_hide_mail: Esconder endereço de e-mail
+ field_comments: Comentário
+ field_url: URL
+ field_start_page: Página inicial
+ field_subproject: Subprojecto
+ field_hours: Horas
+ field_activity: Actividade
+ field_spent_on: Data
+ field_identifier: Identificador
+ field_is_filter: Usado como filtro
+ field_issue_to: Tarefa relacionada
+ field_delay: Atraso
+ field_assignable: As tarefas podem ser associados a este papel
+ field_redirect_existing_links: Redireccionar links existentes
+ field_estimated_hours: Tempo estimado
+ field_column_names: Colunas
+ field_time_zone: Fuso horário
+ field_searchable: Procurável
+ field_default_value: Valor por omissão
+ field_comments_sorting: Mostrar comentários
+ field_parent_title: Página pai
+
+ setting_app_title: Título da aplicação
+ setting_app_subtitle: Sub-título da aplicação
+ setting_welcome_text: Texto de boas vindas
+ setting_default_language: Língua por omissão
+ setting_login_required: Autenticação obrigatória
+ setting_self_registration: Auto-registo
+ setting_attachment_max_size: Tamanho máximo do anexo
+ setting_issues_export_limit: Limite de exportação das tarefas
+ setting_mail_from: E-mail enviado de
+ setting_bcc_recipients: Recipientes de BCC
+ setting_host_name: Hostname
+ setting_text_formatting: Formatação do texto
+ setting_wiki_compression: Compressão do histórico do Wiki
+ setting_feeds_limit: Limite de conteúdo do feed
+ setting_default_projects_public: Projectos novos são públicos por omissão
+ setting_autofetch_changesets: Buscar automaticamente commits
+ setting_sys_api_enabled: Activar Web Service para gestão do repositório
+ setting_commit_ref_keywords: Palavras-chave de referência
+ setting_commit_fix_keywords: Palavras-chave de fecho
+ setting_autologin: Login automático
+ setting_date_format: Formato da data
+ setting_time_format: Formato do tempo
+ setting_cross_project_issue_relations: Permitir relações entre tarefas de projectos diferentes
+ setting_issue_list_default_columns: Colunas na lista de tarefas por omissão
+ setting_repositories_encodings: Encodings dos repositórios
+ setting_commit_logs_encoding: Encoding das mensagens de commit
+ setting_emails_footer: Rodapé do e-mails
+ setting_protocol: Protocolo
+ setting_per_page_options: Opções de objectos por página
+ setting_user_format: Formato de apresentaão de utilizadores
+ setting_activity_days_default: Dias mostrados na actividade do projecto
+ setting_display_subprojects_issues: Mostrar as tarefas dos sub-projectos nos projectos principais
+ setting_enabled_scm: Activar SCM
+ setting_mail_handler_api_enabled: Activar Web Service para e-mails recebidos
+ setting_mail_handler_api_key: Chave da API
+ setting_sequential_project_identifiers: Gerar identificadores de projecto sequênciais
+
+ project_module_issue_tracking: Tarefas
+ project_module_time_tracking: Registo de tempo
+ project_module_news: Notícias
+ project_module_documents: Documentos
+ project_module_files: Ficheiros
+ project_module_wiki: Wiki
+ project_module_repository: Repositório
+ project_module_boards: Forum
+
+ label_user: Utilizador
+ label_user_plural: Utilizadores
+ label_user_new: Novo utilizador
+ label_project: Projecto
+ label_project_new: Novo projecto
+ label_project_plural: Projectos
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Todos os projectos
+ label_project_latest: Últimos projectos
+ label_issue: Tarefa
+ label_issue_new: Nova tarefa
+ label_issue_plural: Tarefas
+ label_issue_view_all: Ver todas as tarefas
+ label_issues_by: "Tarefas por {{value}}"
+ label_issue_added: Tarefa adicionada
+ label_issue_updated: Tarefa actualizada
+ label_document: Documento
+ label_document_new: Novo documento
+ label_document_plural: Documentos
+ label_document_added: Documento adicionado
+ label_role: Papel
+ label_role_plural: Papéis
+ label_role_new: Novo papel
+ label_role_and_permissions: Papéis e permissões
+ label_member: Membro
+ label_member_new: Novo membro
+ label_member_plural: Membros
+ label_tracker: Tipo
+ label_tracker_plural: Tipos
+ label_tracker_new: Novo tipo
+ label_workflow: Workflow
+ label_issue_status: Estado da tarefa
+ label_issue_status_plural: Estados da tarefa
+ label_issue_status_new: Novo estado
+ label_issue_category: Categoria de tarefa
+ label_issue_category_plural: Categorias de tarefa
+ label_issue_category_new: Nova categoria
+ label_custom_field: Campo personalizado
+ label_custom_field_plural: Campos personalizados
+ label_custom_field_new: Novo campo personalizado
+ label_enumerations: Enumerações
+ label_enumeration_new: Novo valor
+ label_information: Informação
+ label_information_plural: Informações
+ label_please_login: Por favor autentique-se
+ label_register: Registar
+ label_password_lost: Perdi a palavra-chave
+ label_home: Página Inicial
+ label_my_page: Página Pessoal
+ label_my_account: Minha conta
+ label_my_projects: Meus projectos
+ label_administration: Administração
+ label_login: Entrar
+ label_logout: Sair
+ label_help: Ajuda
+ label_reported_issues: Tarefas criadas
+ label_assigned_to_me_issues: Tarefas atribuídas a mim
+ label_last_login: Último acesso
+ label_registered_on: Registado em
+ label_activity: Actividade
+ label_overall_activity: Actividade geral
+ label_new: Novo
+ label_logged_as: Ligado como
+ label_environment: Ambiente
+ label_authentication: Autenticação
+ label_auth_source: Modo de autenticação
+ label_auth_source_new: Novo modo de autenticação
+ label_auth_source_plural: Modos de autenticação
+ label_subproject_plural: Sub-projectos
+ label_and_its_subprojects: "{{value}} e sub-projectos"
+ label_min_max_length: Tamanho mínimo-máximo
+ label_list: Lista
+ label_date: Data
+ label_integer: Inteiro
+ label_float: Decimal
+ label_boolean: Booleano
+ label_string: Texto
+ label_text: Texto longo
+ label_attribute: Atributo
+ label_attribute_plural: Atributos
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Sem dados para mostrar
+ label_change_status: Mudar estado
+ label_history: Histórico
+ label_attachment: Ficheiro
+ label_attachment_new: Novo ficheiro
+ label_attachment_delete: Apagar ficheiro
+ label_attachment_plural: Ficheiros
+ label_file_added: Ficheiro adicionado
+ label_report: Relatório
+ label_report_plural: Relatórios
+ label_news: Notícia
+ label_news_new: Nova notícia
+ label_news_plural: Notícias
+ label_news_latest: Últimas notícias
+ label_news_view_all: Ver todas as notícias
+ label_news_added: Notícia adicionada
+ label_change_log: Change log
+ label_settings: Configurações
+ label_overview: Visão geral
+ label_version: Versão
+ label_version_new: Nova versão
+ label_version_plural: Versões
+ label_confirmation: Confirmação
+ label_export_to: 'Também disponível em:'
+ label_read: Ler...
+ label_public_projects: Projectos públicos
+ label_open_issues: aberto
+ label_open_issues_plural: abertos
+ label_closed_issues: fechado
+ label_closed_issues_plural: fechados
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Total
+ label_permissions: Permissões
+ label_current_status: Estado actual
+ label_new_statuses_allowed: Novos estados permitidos
+ label_all: todos
+ label_none: nenhum
+ label_nobody: ninguém
+ label_next: Próximo
+ label_previous: Anterior
+ label_used_by: Usado por
+ label_details: Detalhes
+ label_add_note: Adicionar nota
+ label_per_page: Por página
+ label_calendar: Calendário
+ label_months_from: meses de
+ label_gantt: Gantt
+ label_internal: Interno
+ label_last_changes: "últimas {{count}} alterações"
+ label_change_view_all: Ver todas as alterações
+ label_personalize_page: Personalizar esta página
+ label_comment: Comentário
+ label_comment_plural: Comentários
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Adicionar comentário
+ label_comment_added: Comentário adicionado
+ label_comment_delete: Apagar comentários
+ label_query: Consulta personalizada
+ label_query_plural: Consultas personalizadas
+ label_query_new: Nova consulta
+ label_filter_add: Adicionar filtro
+ label_filter_plural: Filtros
+ label_equals: é
+ label_not_equals: não é
+ label_in_less_than: em menos de
+ label_in_more_than: em mais de
+ label_in: em
+ label_today: hoje
+ label_all_time: sempre
+ label_yesterday: ontem
+ label_this_week: esta semana
+ label_last_week: semana passada
+ label_last_n_days: "últimos {{count}} dias"
+ label_this_month: este mês
+ label_last_month: mês passado
+ label_this_year: este ano
+ label_date_range: Date range
+ label_less_than_ago: menos de dias atrás
+ label_more_than_ago: mais de dias atrás
+ label_ago: dias atrás
+ label_contains: contém
+ label_not_contains: não contém
+ label_day_plural: dias
+ label_repository: Repositório
+ label_repository_plural: Repositórios
+ label_browse: Navegar
+ label_modification: "{{count}} alteração"
+ label_modification_plural: "{{count}} alterações"
+ label_revision: Revisão
+ label_revision_plural: Revisões
+ label_associated_revisions: Revisões associadas
+ label_added: adicionado
+ label_modified: modificado
+ label_copied: copiado
+ label_renamed: renomeado
+ label_deleted: apagado
+ label_latest_revision: Última revisão
+ label_latest_revision_plural: Últimas revisões
+ label_view_revisions: Ver revisões
+ label_max_size: Tamanho máximo
+ label_sort_highest: Mover para o início
+ label_sort_higher: Mover para cima
+ label_sort_lower: Mover para baixo
+ label_sort_lowest: Mover para o fim
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Termina em {{value}}"
+ label_roadmap_overdue: "Atrasado {{value}}"
+ label_roadmap_no_issues: Sem tarefas para esta versão
+ label_search: Procurar
+ label_result_plural: Resultados
+ label_all_words: Todas as palavras
+ label_wiki: Wiki
+ label_wiki_edit: Edição da Wiki
+ label_wiki_edit_plural: Edições da Wiki
+ label_wiki_page: Página da Wiki
+ label_wiki_page_plural: Páginas da Wiki
+ label_index_by_title: Índice por título
+ label_index_by_date: Índice por data
+ label_current_version: Versão actual
+ label_preview: Pré-visualizar
+ label_feed_plural: Feeds
+ label_changes_details: Detalhes de todas as mudanças
+ label_issue_tracking: Tarefas
+ label_spent_time: Tempo gasto
+ label_f_hour: "{{value}} hora"
+ label_f_hour_plural: "{{value}} horas"
+ label_time_tracking: Registo de tempo
+ label_change_plural: Mudanças
+ label_statistics: Estatísticas
+ label_commits_per_month: Commits por mês
+ label_commits_per_author: Commits por autor
+ label_view_diff: Ver diferenças
+ label_diff_inline: inline
+ label_diff_side_by_side: lado a lado
+ label_options: Opções
+ label_copy_workflow_from: Copiar workflow de
+ label_permissions_report: Relatório de permissões
+ label_watched_issues: Tarefas observadas
+ label_related_issues: Tarefas relacionadas
+ label_applied_status: Estado aplicado
+ label_loading: A carregar...
+ label_relation_new: Nova relação
+ label_relation_delete: Apagar relação
+ label_relates_to: relacionado a
+ label_duplicates: duplica
+ label_duplicated_by: duplicado por
+ label_blocks: bloqueia
+ label_blocked_by: bloqueado por
+ label_precedes: precede
+ label_follows: segue
+ label_end_to_start: fim a início
+ label_end_to_end: fim a fim
+ label_start_to_start: início a início
+ label_start_to_end: início a fim
+ label_stay_logged_in: Guardar sessão
+ label_disabled: desactivado
+ label_show_completed_versions: Mostrar versões acabadas
+ label_me: eu
+ label_board: Forum
+ label_board_new: Novo forum
+ label_board_plural: Forums
+ label_topic_plural: Tópicos
+ label_message_plural: Mensagens
+ label_message_last: Última mensagem
+ label_message_new: Nova mensagem
+ label_message_posted: Mensagem adicionada
+ label_reply_plural: Respostas
+ label_send_information: Enviar dados da conta para o utilizador
+ label_year: Ano
+ label_month: mês
+ label_week: Semana
+ label_date_from: De
+ label_date_to: Para
+ label_language_based: Baseado na língua do utilizador
+ label_sort_by: "Ordenar por {{value}}"
+ label_send_test_email: enviar um e-mail de teste
+ label_feeds_access_key_created_on: "Chave RSS criada há {{value}} atrás"
+ label_module_plural: Módulos
+ label_added_time_by: "Adicionado por {{author}} há {{age}} atrás"
+ label_updated_time: "Alterado há {{value}} atrás"
+ label_jump_to_a_project: Ir para o projecto...
+ label_file_plural: Ficheiros
+ label_changeset_plural: Changesets
+ label_default_columns: Colunas por omissão
+ label_no_change_option: (sem alteração)
+ label_bulk_edit_selected_issues: Editar tarefas seleccionadas em conjunto
+ label_theme: Tema
+ label_default: Padrão
+ label_search_titles_only: Procurar apenas em títulos
+ label_user_mail_option_all: "Para qualquer evento em todos os meus projectos"
+ label_user_mail_option_selected: "Para qualquer evento apenas nos projectos seleccionados..."
+ label_user_mail_option_none: "Apenas para coisas que esteja a observar ou esteja envolvido"
+ label_user_mail_no_self_notified: "Não quero ser notificado de alterações feitas por mim"
+ label_registration_activation_by_email: Activação da conta por e-mail
+ label_registration_manual_activation: Activação manual da conta
+ label_registration_automatic_activation: Activação automática da conta
+ label_display_per_page: "Por página: {{value}}"
+ label_age: Idade
+ label_change_properties: Mudar propriedades
+ label_general: Geral
+ label_more: Mais
+ label_scm: SCM
+ label_plugins: Extensões
+ label_ldap_authentication: Autenticação LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Descrição opcional
+ label_add_another_file: Adicionar outro ficheiro
+ label_preferences: Preferências
+ label_chronological_order: Em ordem cronológica
+ label_reverse_chronological_order: Em ordem cronológica inversa
+ label_planning: Planeamento
+ label_incoming_emails: E-mails a chegar
+ label_generate_key: Gerar uma chave
+ label_issue_watchers: Observadores
+
+ button_login: Entrar
+ button_submit: Submeter
+ button_save: Guardar
+ button_check_all: Marcar tudo
+ button_uncheck_all: Desmarcar tudo
+ button_delete: Apagar
+ button_create: Criar
+ button_test: Testar
+ button_edit: Editar
+ button_add: Adicionar
+ button_change: Alterar
+ button_apply: Aplicar
+ button_clear: Limpar
+ button_lock: Bloquear
+ button_unlock: Desbloquear
+ button_download: Download
+ button_list: Listar
+ button_view: Ver
+ button_move: Mover
+ button_back: Voltar
+ button_cancel: Cancelar
+ button_activate: Activar
+ button_sort: Ordenar
+ button_log_time: Tempo de trabalho
+ button_rollback: Voltar para esta versão
+ button_watch: Observar
+ button_unwatch: Deixar de observar
+ button_reply: Responder
+ button_archive: Arquivar
+ button_unarchive: Desarquivar
+ button_reset: Reinicializar
+ button_rename: Renomear
+ button_change_password: Mudar palavra-chave
+ button_copy: Copiar
+ button_annotate: Anotar
+ button_update: Actualizar
+ button_configure: Configurar
+ button_quote: Citar
+
+ status_active: activo
+ status_registered: registado
+ status_locked: bloqueado
+
+ text_select_mail_notifications: Seleccionar as acções que originam uma notificação por e-mail.
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 siginifica sem restrição
+ text_project_destroy_confirmation: Tem a certeza que deseja apagar o projecto e todos os dados relacionados?
+ text_subprojects_destroy_warning: "O(s) seu(s) sub-projecto(s): {{value}} também será/serão apagado(s)."
+ text_workflow_edit: Seleccione um papel e um tipo de tarefa para editar o workflow
+ text_are_you_sure: Tem a certeza?
+ text_tip_task_begin_day: tarefa a começar neste dia
+ text_tip_task_end_day: tarefa a acabar neste dia
+ text_tip_task_begin_end_day: tarefa a começar e acabar neste dia
+ text_project_identifier_info: 'Apenas são permitidos letras minúsculas (a-z), números e hífens.<br />Uma vez guardado, o identificador não poderá ser alterado.'
+ text_caracters_maximum: "máximo {{count}} caracteres."
+ text_caracters_minimum: "Deve ter pelo menos {{count}} caracteres."
+ text_length_between: "Deve ter entre {{min}} e {{max}} caracteres."
+ text_tracker_no_workflow: Sem workflow definido para este tipo de tarefa.
+ text_unallowed_characters: Caracteres não permitidos
+ text_comma_separated: Permitidos múltiplos valores (separados por vírgula).
+ text_issues_ref_in_commit_messages: Referenciando e fechando tarefas em mensagens de commit
+ text_issue_added: "Tarefa {{id}} foi criada por {{author}}."
+ text_issue_updated: "Tarefa {{id}} foi actualizada por {{author}}."
+ text_wiki_destroy_confirmation: Tem a certeza que deseja apagar este wiki e todo o seu conteúdo?
+ text_issue_category_destroy_question: "Algumas tarefas ({{count}}) estão atribuídas a esta categoria. O que quer fazer?"
+ text_issue_category_destroy_assignments: Remover as atribuições à categoria
+ text_issue_category_reassign_to: Re-atribuir as tarefas para esta categoria
+ text_user_mail_option: "Para projectos não seleccionados, apenas receberá notificações acerca de coisas que está a observar ou está envolvido (ex. tarefas das quais foi o criador ou lhes foram atribuídas)."
+ text_no_configuration_data: "Papeis, tipos de tarefas, estados das tarefas e workflows ainda não foram configurados.\nÉ extremamente recomendado carregar as configurações padrão. Será capaz de as modificar depois de estarem carregadas."
+ text_load_default_configuration: Carregar as configurações padrão
+ text_status_changed_by_changeset: "Aplicado no changeset {{value}}."
+ text_issues_destroy_confirmation: 'Tem a certeza que deseja apagar a(s) tarefa(s) seleccionada(s)?'
+ text_select_project_modules: 'Seleccione os módulos a activar para este projecto:'
+ text_default_administrator_account_changed: Conta default de administrador alterada.
+ text_file_repository_writable: Repositório de ficheiros com permissões de escrita
+ text_rmagick_available: RMagick disponível (opcional)
+ text_destroy_time_entries_question: "{{hours}} horas de trabalho foram atribuídas a estas tarefas que vai apagar. O que deseja fazer?"
+ text_destroy_time_entries: Apagar as horas
+ text_assign_time_entries_to_project: Atribuir as horas ao projecto
+ text_reassign_time_entries: 'Re-atribuir as horas para esta tarefa:'
+ text_user_wrote: "{{value}} escreveu:"
+ text_enumeration_destroy_question: "{{count}} objectos estão atribuídos a este valor."
+ text_enumeration_category_reassign_to: 'Re-atribuí-los para este valor:'
+ text_email_delivery_not_configured: "Entrega por e-mail não está configurada, e as notificação estão desactivadas.\nConfigure o seu servidor de SMTP em config/email.yml e reinicie a aplicação para activar estas funcionalidades."
+
+ default_role_manager: Gestor
+ default_role_developper: Programador
+ default_role_reporter: Repórter
+ default_tracker_bug: Bug
+ default_tracker_feature: Funcionalidade
+ default_tracker_support: Suporte
+ default_issue_status_new: Novo
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Resolvido
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Fechado
+ default_issue_status_rejected: Rejeitado
+ default_doc_category_user: Documentação de utilizador
+ default_doc_category_tech: Documentação técnica
+ default_priority_low: Baixa
+ default_priority_normal: Normal
+ default_priority_high: Alta
+ default_priority_urgent: Urgente
+ default_priority_immediate: Imediata
+ default_activity_design: Planeamento
+ default_activity_development: Desenvolvimento
+
+ enumeration_issue_priorities: Prioridade de tarefas
+ enumeration_doc_categories: Categorias de documentos
+ enumeration_activities: Actividades (Registo de tempo)
+ setting_plain_text_mail: Apenas texto simples (sem HTML)
+ permission_view_files: Ver ficheiros
+ permission_edit_issues: Editar tarefas
+ permission_edit_own_time_entries: Editar horas pessoais
+ permission_manage_public_queries: Gerir queries públicas
+ permission_add_issues: Adicionar tarefas
+ permission_log_time: Registar tempo gasto
+ permission_view_changesets: Ver changesets
+ permission_view_time_entries: Ver tempo gasto
+ permission_manage_versions: Gerir versões
+ permission_manage_wiki: Gerir wiki
+ permission_manage_categories: Gerir categorias de tarefas
+ permission_protect_wiki_pages: Proteger páginas de wiki
+ permission_comment_news: Comentar notícias
+ permission_delete_messages: Apagar mensagens
+ permission_select_project_modules: Seleccionar módulos do projecto
+ permission_manage_documents: Gerir documentos
+ permission_edit_wiki_pages: Editar páginas de wiki
+ permission_add_issue_watchers: Adicionar observadores
+ permission_view_gantt: ver diagrama de Gantt
+ permission_move_issues: Mover tarefas
+ permission_manage_issue_relations: Gerir relações de tarefas
+ permission_delete_wiki_pages: Apagar páginas de wiki
+ permission_manage_boards: Gerir forums
+ permission_delete_wiki_pages_attachments: Apagar anexos
+ permission_view_wiki_edits: Ver histórico da wiki
+ permission_add_messages: Submeter mensagens
+ permission_view_messages: Ver mensagens
+ permission_manage_files: Gerir ficheiros
+ permission_edit_issue_notes: Editar notas de tarefas
+ permission_manage_news: Gerir notícias
+ permission_view_calendar: Ver calendário
+ permission_manage_members: Gerir membros
+ permission_edit_messages: Editar mensagens
+ permission_delete_issues: Apagar tarefas
+ permission_view_issue_watchers: Ver lista de observadores
+ permission_manage_repository: Gerir repositório
+ permission_commit_access: Acesso a submissão
+ permission_browse_repository: Navegar em repositório
+ permission_view_documents: Ver documentos
+ permission_edit_project: Editar projecto
+ permission_add_issue_notes: Adicionar notas a tarefas
+ permission_save_queries: Guardar queries
+ permission_view_wiki_pages: Ver wiki
+ permission_rename_wiki_pages: Renomear páginas de wiki
+ permission_edit_time_entries: Editar entradas de tempo
+ permission_edit_own_issue_notes: Editar as prórpias notas
+ setting_gravatar_enabled: Utilizar icons Gravatar
+ label_example: Exemplo
+ text_repository_usernames_mapping: "Seleccionar ou actualizar o utilizador de Redmine mapeado a cada nome de utilizador encontrado no repositório.\nUtilizadores com o mesmo nome de utilizador ou email no Redmine e no repositório são mapeados automaticamente."
+ permission_edit_own_messages: Editar as próprias mensagens
+ permission_delete_own_messages: Apagar as próprias mensagens
+ label_user_activity: "Actividade de {{value}}"
+ label_updated_time_by: "Actualizado por {{author}} há {{age}}"
+ text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser mostrado.'
+ setting_diff_max_lines_displayed: Número máximo de linhas de diff mostradas
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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").
+ label_group_plural: Groups
+ label_group: Group
+ label_group_new: New group
+ label_time_entry_plural: Spent time
+ text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
+ text_journal_set_to: "{{label}} set to {{value}}"
+ text_journal_deleted: "{{label}} deleted ({{old}})"
+ 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
--- /dev/null
+ro:
+ date:
+ formats:
+ default: "%d-%m-%Y"
+ short: "%d %b"
+ long: "%d %B %Y"
+ only_day: "%e"
+
+ day_names: [Duminică, Luni, Marti, Miercuri, Joi, Vineri, Sâmbătă]
+ abbr_day_names: [Dum, Lun, Mar, Mie, Joi, Vin, Sâm]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Ianuarie, Februarie, Martie, Aprilie, Mai, Iunie, Iulie, August, Septembrie, Octombrie, Noiembrie, Decembrie]
+ abbr_month_names: [~, Ian, Feb, Mar, Apr, Mai, Iun, Iul, Aug, Sep, Oct, Noi, Dec]
+ # Used in date_select and datime_select.
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%m/%d/%Y %I:%M %p"
+ time: "%I:%M %p"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "jumătate de minut"
+ less_than_x_seconds:
+ one: "mai puțin de o secundă"
+ other: "mai puțin de {{count}} secunde"
+ x_seconds:
+ one: "o secundă"
+ other: "{{count}} secunde"
+ less_than_x_minutes:
+ one: "mai puțin de un minut"
+ other: "mai puțin de {{count}} minute"
+ x_minutes:
+ one: "un minut"
+ other: "{{count}} minute"
+ about_x_hours:
+ one: "aproximativ o oră"
+ other: "aproximativ {{count}} ore"
+ x_days:
+ one: "o zi"
+ other: "{{count}} zile"
+ about_x_months:
+ one: "aproximativ o lună"
+ other: "aproximativ {{count}} luni"
+ x_months:
+ one: "o luna"
+ other: "{{count}} luni"
+ about_x_years:
+ one: "aproximativ un an"
+ other: "aproximativ {{count}} ani"
+ over_x_years:
+ one: "peste un an"
+ other: "peste {{count}} ani"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "și"
+ skip_last_comma: true
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "nu este inclus în listă"
+ exclusion: "este rezervat"
+ invalid: "nu este valid"
+ confirmation: "nu este identică"
+ accepted: "trebuie acceptat"
+ empty: "trebuie completat"
+ blank: "nu poate fi gol"
+ too_long: "este prea lung"
+ too_short: "este prea scurt"
+ wrong_length: "nu are lungimea corectă"
+ taken: "a fost luat deja"
+ not_a_number: "nu este un număr"
+ not_a_date: "nu este o dată validă"
+ greater_than: "trebuie să fie mai mare de {{count}}"
+ greater_than_or_equal_to: "trebuie să fie mai mare sau egal cu {{count}}"
+ equal_to: "trebuie să fie egal cu {count}}"
+ less_than: "trebuie să fie mai mic decat {{count}}"
+ less_than_or_equal_to: "trebuie să fie mai mic sau egal cu {{count}}"
+ odd: "trebuie să fie impar"
+ even: "trebuie să fie par"
+ greater_than_start_date: "trebuie să fie după data de început"
+ not_same_project: "trebuie să aparțină aceluiași proiect"
+ circular_dependency: "Această relație ar crea o dependență circulară"
+
+ actionview_instancetag_blank_option: Selectați
+
+ general_text_No: 'Nu'
+ general_text_Yes: 'Da'
+ general_text_no: 'nu'
+ general_text_yes: 'da'
+ general_lang_name: 'Română'
+ general_csv_separator: '.'
+ general_csv_decimal_separator: ','
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '2'
+
+ notice_account_updated: Cont actualizat.
+ notice_account_invalid_creditentials: Utilizator sau parola nevalidă
+ notice_account_password_updated: Parolă actualizată.
+ notice_account_wrong_password: Parolă greșită
+ notice_account_register_done: Contul a fost creat. Pentru activare, urmați legătura trimisă prin email.
+ notice_account_unknown_email: Utilizator necunoscut.
+ notice_can_t_change_password: Acest cont folosește o sursă externă de autentificare. Nu se poate schimba parola.
+ notice_account_lost_email_sent: S-a trimis un email cu instrucțiuni de schimbare a parolei.
+ notice_account_activated: Contul a fost activat. Vă puteți autentifica acum.
+ notice_successful_create: Creat.
+ notice_successful_update: Actualizat.
+ notice_successful_delete: Șters.
+ notice_successful_connection: Conectat.
+ notice_file_not_found: Pagina pe care doriți să o accesați nu există sau a fost ștearsă.
+ notice_locking_conflict: Datele au fost actualizate de alt utilizator.
+ notice_not_authorized: Nu sunteți autorizat sa accesați această pagină.
+ notice_email_sent: "S-a trimis un email către {{value}}"
+ notice_email_error: "A intervenit o eroare la trimiterea de email ({{value}})"
+ notice_feeds_access_key_reseted: Cheia de acces RSS a fost resetată.
+ notice_failed_to_save_issues: "Nu s-au putut salva {{count}} tichete din cele {{total}} selectate: {{ids}}."
+ notice_no_issue_selected: "Niciun tichet selectat! Vă rugăm să selectați tichetele pe care doriți să le editați."
+ notice_account_pending: "Contul dumneavoastră a fost creat și așteaptă aprobarea administratorului."
+ notice_default_data_loaded: S-a încărcat configurația implicită.
+ notice_unable_delete_version: Nu se poate șterge versiunea.
+
+ error_can_t_load_default_data: "Nu s-a putut încărca configurația implicită: {{value}}"
+ error_scm_not_found: "Nu s-a găsit articolul sau revizia în depozit."
+ error_scm_command_failed: "A intervenit o eroare la accesarea depozitului: {{value}}"
+ error_scm_annotate: "Nu există sau nu poate fi adnotată."
+ error_issue_not_found_in_project: 'Tichetul nu a fost găsit sau nu aparține acestui proiect'
+
+ warning_attachments_not_saved: "Nu s-au putut salva {{count}} fișiere."
+
+ mail_subject_lost_password: "Parola dumneavoastră: {{value}}"
+ mail_body_lost_password: 'Pentru a schimba parola, accesați:'
+ mail_subject_register: "Activarea contului {{value}}"
+ mail_body_register: 'Pentru activarea contului, accesați:'
+ mail_body_account_information_external: "Puteți folosi contul „{value}}” pentru a vă autentifica."
+ mail_body_account_information: Informații despre contul dumneavoastră
+ mail_subject_account_activation_request: "Cerere de activare a contului {{value}}"
+ mail_body_account_activation_request: "S-a înregistrat un utilizator nou ({{value}}). Contul așteaptă aprobarea dumneavoastră:"
+ mail_subject_reminder: "{{count}} tichete trebuie rezolvate în următoarele zile"
+ mail_body_reminder: "{{count}} tichete atribuite dumneavoastră trebuie rezolvate în următoarele {{days}} zile:"
+
+ gui_validation_error: o eroare
+ gui_validation_error_plural: "{{count}} erori"
+
+ field_name: Nume
+ field_description: Descriere
+ field_summary: Rezumat
+ field_is_required: Obligatoriu
+ field_firstname: Prenume
+ field_lastname: Nume
+ field_mail: Email
+ field_filename: Fișier
+ field_filesize: Mărime
+ field_downloads: Descărcări
+ field_author: Autor
+ field_created_on: Creat la
+ field_updated_on: Actualizat la
+ field_field_format: Format
+ field_is_for_all: Pentru toate proiectele
+ field_possible_values: Valori posibile
+ field_regexp: Expresie regulară
+ field_min_length: lungime minimă
+ field_max_length: lungime maximă
+ field_value: Valoare
+ field_category: Categorie
+ field_title: Titlu
+ field_project: Proiect
+ field_issue: Tichet
+ field_status: Stare
+ field_notes: Note
+ field_is_closed: Rezolvat
+ field_is_default: Implicit
+ field_tracker: Tip de tichet
+ field_subject: Subiect
+ field_due_date: Data finalizării
+ field_assigned_to: Atribuit
+ field_priority: Prioritate
+ field_fixed_version: Versiune țintă
+ field_user: Utilizator
+ field_role: Rol
+ field_homepage: Pagina principală
+ field_is_public: Public
+ field_parent: Sub-proiect al
+ field_is_in_chlog: Tichete afișate în jurnalul de activitate
+ field_is_in_roadmap: Tichete afișate în plan
+ field_login: Autentificare
+ field_mail_notification: Notificări prin e-mail
+ field_admin: Administrator
+ field_last_login_on: Ultima autentificare în
+ field_language: Limba
+ field_effective_date: Data
+ field_password: Parola
+ field_new_password: Parola nouă
+ field_password_confirmation: Confirmare
+ field_version: Versiune
+ field_type: Tip
+ field_host: Gazdă
+ field_port: Port
+ field_account: Cont
+ field_base_dn: Base DN
+ field_attr_login: Atribut autentificare
+ field_attr_firstname: Atribut prenume
+ field_attr_lastname: Atribut nume
+ field_attr_mail: Atribut email
+ field_onthefly: Creare utilizator pe loc
+ field_start_date: Data începerii
+ field_done_ratio: Realizat (%)
+ field_auth_source: Mod autentificare
+ field_hide_mail: Nu se afișează adresa de email
+ field_comments: Comentariu
+ field_url: URL
+ field_start_page: Pagina de start
+ field_subproject: Subproiect
+ field_hours: Ore
+ field_activity: Activitate
+ field_spent_on: Data
+ field_identifier: Identificator
+ field_is_filter: Filtru
+ field_issue_to: Tichet asociat
+ field_delay: Întârziere
+ field_assignable: Se pot atribui tichete acestui rol
+ field_redirect_existing_links: Redirecționează legăturile existente
+ field_estimated_hours: Timp estimat
+ field_column_names: Coloane
+ field_time_zone: Fus orar
+ field_searchable: Căutare
+ field_default_value: Valoare implicita
+ field_comments_sorting: Afișează comentarii
+ field_parent_title: Pagina superioara
+ field_editable: Modificabil
+ field_watcher: Urmărește
+ field_identity_url: URL OpenID
+ field_content: Conținut
+
+ setting_app_title: Titlu aplicație
+ setting_app_subtitle: Subtitlu aplicație
+ setting_welcome_text: Text de întâmpinare
+ setting_default_language: Limba implicita
+ setting_login_required: Necesita autentificare
+ setting_self_registration: Înregistrare automată
+ setting_attachment_max_size: Mărime maxima atașament
+ setting_issues_export_limit: Limită de tichete exportate
+ setting_mail_from: Adresa de email a expeditorului
+ setting_bcc_recipients: Alți destinatari pentru email (BCC)
+ setting_plain_text_mail: Mesaje text (fără HTML)
+ setting_host_name: Numele gazdei și calea
+ setting_text_formatting: Formatare text
+ setting_wiki_compression: Comprimare istoric Wiki
+ setting_feeds_limit: Limita de actualizări din feed
+ setting_default_projects_public: Proiectele noi sunt implicit publice
+ setting_autofetch_changesets: Preluare automată a modificărilor din depozit
+ setting_sys_api_enabled: Activare WS pentru gestionat depozitul
+ setting_commit_ref_keywords: Cuvinte cheie pt. referire tichet
+ setting_commit_fix_keywords: Cuvinte cheie pt. rezolvare tichet
+ setting_autologin: Autentificare automată
+ setting_date_format: Format dată
+ setting_time_format: Format oră
+ setting_cross_project_issue_relations: Permite legături de tichete între proiecte
+ setting_issue_list_default_columns: Coloane implicite afișate în lista de tichete
+ setting_repositories_encodings: Codare pentru depozit
+ setting_commit_logs_encoding: Codare pentru mesaje
+ setting_emails_footer: Subsol email
+ setting_protocol: Protocol
+ setting_per_page_options: Număr de obiecte pe pagină
+ setting_user_format: Stil de afișare pentru utilizator
+ setting_activity_days_default: Se afișează zile în jurnalul proiectului
+ setting_display_subprojects_issues: Afișează implicit tichetele sub-proiectelor în proiectele principale
+ setting_enabled_scm: SCM activat
+ setting_mail_handler_api_enabled: Activare WS pentru email primit
+ setting_mail_handler_api_key: cheie API
+ setting_sequential_project_identifiers: Generează secvențial identificatoarele de proiect
+ setting_gravatar_enabled: Folosește poze Gravatar pentru utilizatori
+ setting_diff_max_lines_displayed: Număr maxim de linii de diferență afișate
+ setting_file_max_size_displayed: Număr maxim de fișiere text afișate în pagină (inline)
+ setting_repository_log_display_limit: Număr maxim de revizii afișate în istoricul fișierului
+ setting_openid: Permite înregistrare și autentificare cu OpenID
+
+ permission_edit_project: Editează proiectul
+ permission_select_project_modules: Alege module pentru proiect
+ permission_manage_members: Editează membri
+ permission_manage_versions: Editează versiuni
+ permission_manage_categories: Editează categorii
+ permission_add_issues: Adaugă tichete
+ permission_edit_issues: Editează tichete
+ permission_manage_issue_relations: Editează relații tichete
+ permission_add_issue_notes: Adaugă note
+ permission_edit_issue_notes: Editează note
+ permission_edit_own_issue_notes: Editează notele proprii
+ permission_move_issues: Mută tichete
+ permission_delete_issues: Șterge tichete
+ permission_manage_public_queries: Editează căutările implicite
+ permission_save_queries: Salvează căutările
+ permission_view_gantt: Afișează Gantt
+ permission_view_calendar: Afișează calendarul
+ permission_view_issue_watchers: Afișează lista de persoane interesate
+ permission_add_issue_watchers: Adaugă persoane interesate
+ permission_log_time: Înregistrează timpul de lucru
+ permission_view_time_entries: Afișează timpul de lucru
+ permission_edit_time_entries: Editează jurnalele cu timp de lucru
+ permission_edit_own_time_entries: Editează jurnalele proprii cu timpul de lucru
+ permission_manage_news: Editează știri
+ permission_comment_news: Comentează știrile
+ permission_manage_documents: Editează documente
+ permission_view_documents: Afișează documente
+ permission_manage_files: Editează fișiere
+ permission_view_files: Afișează fișiere
+ permission_manage_wiki: Editează wiki
+ permission_rename_wiki_pages: Redenumește pagini wiki
+ permission_delete_wiki_pages: Șterge pagini wiki
+ permission_view_wiki_pages: Afișează wiki
+ permission_view_wiki_edits: Afișează istoricul wiki
+ permission_edit_wiki_pages: Editează pagini wiki
+ permission_delete_wiki_pages_attachments: Șterge atașamente
+ permission_protect_wiki_pages: Blochează pagini wiki
+ permission_manage_repository: Gestionează depozitul
+ permission_browse_repository: Răsfoiește depozitul
+ permission_view_changesets: Afișează modificările din depozit
+ permission_commit_access: Acces commit
+ permission_manage_boards: Editează forum
+ permission_view_messages: Afișează mesaje
+ permission_add_messages: Scrie mesaje
+ permission_edit_messages: Editează mesaje
+ permission_edit_own_messages: Editează mesajele proprii
+ permission_delete_messages: Șterge mesaje
+ permission_delete_own_messages: Șterge mesajele proprii
+
+ project_module_issue_tracking: Tichete
+ project_module_time_tracking: Timp de lucru
+ project_module_news: Știri
+ project_module_documents: Documente
+ project_module_files: Fișiere
+ project_module_wiki: Wiki
+ project_module_repository: Depozit
+ project_module_boards: Forum
+
+ label_user: Utilizator
+ label_user_plural: Utilizatori
+ label_user_new: Utilizator nou
+ label_project: Proiect
+ label_project_new: Proiect nou
+ label_project_plural: Proiecte
+ label_x_projects:
+ zero: niciun proiect
+ one: un proiect
+ other: "{{count}} proiecte"
+ label_project_all: Toate proiectele
+ label_project_latest: Proiecte noi
+ label_issue: Tichet
+ label_issue_new: Tichet nou
+ label_issue_plural: Tichete
+ label_issue_view_all: Afișează toate tichetele
+ label_issues_by: "Sortează după {{value}}"
+ label_issue_added: Adaugat
+ label_issue_updated: Actualizat
+ label_document: Document
+ label_document_new: Document nou
+ label_document_plural: Documente
+ label_document_added: Adăugat
+ label_role: Rol
+ label_role_plural: Roluri
+ label_role_new: Rol nou
+ label_role_and_permissions: Roluri și permisiuni
+ label_member: Membru
+ label_member_new: membru nou
+ label_member_plural: Membri
+ label_tracker: Tip de tichet
+ label_tracker_plural: Tipuri de tichete
+ label_tracker_new: Tip nou de tichet
+ label_workflow: Mod de lucru
+ label_issue_status: Stare tichet
+ label_issue_status_plural: Stare tichete
+ label_issue_status_new: Stare nouă
+ label_issue_category: Categorie de tichet
+ label_issue_category_plural: Categorii de tichete
+ label_issue_category_new: Categorie nouă
+ label_custom_field: Câmp personalizat
+ label_custom_field_plural: Câmpuri personalizate
+ label_custom_field_new: Câmp nou personalizat
+ label_enumerations: Enumerări
+ label_enumeration_new: Valoare nouă
+ label_information: Informație
+ label_information_plural: Informații
+ label_please_login: Vă rugăm să vă autentificați
+ label_register: Înregistrare
+ label_login_with_open_id_option: sau autentificare cu OpenID
+ label_password_lost: Parolă uitată
+ label_home: Acasă
+ label_my_page: Pagina mea
+ label_my_account: Contul meu
+ label_my_projects: Proiectele mele
+ label_administration: Administrare
+ label_login: Autentificare
+ label_logout: Ieșire din cont
+ label_help: Ajutor
+ label_reported_issues: Tichete
+ label_assigned_to_me_issues: Tichetele mele
+ label_last_login: Ultima conectare
+ label_registered_on: Înregistrat la
+ label_activity: Activitate
+ label_overall_activity: Activitate - vedere de ansamblu
+ label_user_activity: "Activitate {{value}}"
+ label_new: Nou
+ label_logged_as: Autentificat ca
+ label_environment: Mediu
+ label_authentication: Autentificare
+ label_auth_source: Mod de autentificare
+ label_auth_source_new: Nou
+ label_auth_source_plural: Moduri de autentificare
+ label_subproject_plural: Sub-proiecte
+ label_and_its_subprojects: "{{value}} și sub-proiecte"
+ label_min_max_length: lungime min - max
+ label_list: Listă
+ label_date: Dată
+ label_integer: Întreg
+ label_float: Zecimal
+ label_boolean: Valoare logică
+ label_string: Text
+ label_text: Text lung
+ label_attribute: Atribut
+ label_attribute_plural: Atribute
+ label_download: "{{count}} descărcare"
+ label_download_plural: "{{count}} descărcări"
+ label_no_data: Nu există date de afișat
+ label_change_status: Schimbă starea
+ label_history: Istoric
+ label_attachment: Fișier
+ label_attachment_new: Fișier nou
+ label_attachment_delete: Șterge fișier
+ label_attachment_plural: Fișiere
+ label_file_added: Adăugat
+ label_report: Raport
+ label_report_plural: Rapoarte
+ label_news: Știri
+ label_news_new: Adaugă știre
+ label_news_plural: Știri
+ label_news_latest: Ultimele știri
+ label_news_view_all: Afișează toate știrile
+ label_news_added: Adăugat
+ label_change_log: Istoric
+ label_settings: Setări
+ label_overview: Pagină proiect
+ label_version: Versiune
+ label_version_new: Versiune nouă
+ label_version_plural: Versiuni
+ label_confirmation: Confirmare
+ label_export_to: 'Disponibil și în:'
+ label_read: Citește...
+ label_public_projects: Proiecte publice
+ label_open_issues: deschis
+ label_open_issues_plural: deschise
+ label_closed_issues: închis
+ label_closed_issues_plural: închise
+ label_x_open_issues_abbr_on_total:
+ zero: 0 deschise / {{total}}
+ one: 1 deschis / {{total}}
+ other: "{{count}} deschise / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 deschise
+ one: 1 deschis
+ other: "{{count}} deschise"
+ label_x_closed_issues_abbr:
+ zero: 0 închise
+ one: 1 închis
+ other: "{{count}} închise"
+ label_total: Total
+ label_permissions: Permisiuni
+ label_current_status: Stare curentă
+ label_new_statuses_allowed: Stări noi permise
+ label_all: toate
+ label_none: niciunul
+ label_nobody: nimeni
+ label_next: Înainte
+ label_previous: Înapoi
+ label_used_by: Folosit de
+ label_details: Detalii
+ label_add_note: Adaugă o notă
+ label_per_page: pe pagină
+ label_calendar: Calendar
+ label_months_from: luni de la
+ label_gantt: Gantt
+ label_internal: Intern
+ label_last_changes: "ultimele {{count}} schimbări"
+ label_change_view_all: Afișează toate schimbările
+ label_personalize_page: Personalizează aceasta pagina
+ label_comment: Comentariu
+ label_comment_plural: Comentarii
+ label_x_comments:
+ zero: fara comentarii
+ one: 1 comentariu
+ other: "{{count}} comentarii"
+ label_comment_add: Adaugă un comentariu
+ label_comment_added: Adăugat
+ label_comment_delete: Șterge comentariul
+ label_query: Cautare personalizata
+ label_query_plural: Căutări personalizate
+ label_query_new: Căutare nouă
+ label_filter_add: Adaugă filtru
+ label_filter_plural: Filtre
+ label_equals: este
+ label_not_equals: nu este
+ label_in_less_than: în mai puțin de
+ label_in_more_than: în mai mult de
+ label_in: în
+ label_today: astăzi
+ label_all_time: oricând
+ label_yesterday: ieri
+ label_this_week: săptămâna aceasta
+ label_last_week: săptămâna trecută
+ label_last_n_days: "ultimele {{count}} zile"
+ label_this_month: luna aceasta
+ label_last_month: luna trecută
+ label_this_year: anul acesta
+ label_date_range: Perioada
+ label_less_than_ago: mai puțin de ... zile
+ label_more_than_ago: mai mult de ... zile
+ label_ago: în urma
+ label_contains: conține
+ label_not_contains: nu conține
+ label_day_plural: zile
+ label_repository: Depozit
+ label_repository_plural: Depozite
+ label_browse: Afișează
+ label_modification: "{{count}} schimbare"
+ label_modification_plural: "{{count}} schimbări"
+ label_revision: Revizie
+ label_revision_plural: Revizii
+ label_associated_revisions: Revizii asociate
+ label_added: adaugată
+ label_modified: modificată
+ label_copied: copiată
+ label_renamed: redenumită
+ label_deleted: ștearsă
+ label_latest_revision: Ultima revizie
+ label_latest_revision_plural: Ultimele revizii
+ label_view_revisions: Afișează revizii
+ label_max_size: Mărime maximă
+ label_sort_highest: Prima
+ label_sort_higher: În sus
+ label_sort_lower: În jos
+ label_sort_lowest: Ultima
+ label_roadmap: Planificare
+ label_roadmap_due_in: "De terminat în {{value}}"
+ label_roadmap_overdue: "Întârziat cu {{value}}"
+ label_roadmap_no_issues: Nu există tichete pentru această versiune
+ label_search: Caută
+ label_result_plural: Rezultate
+ label_all_words: toate cuvintele
+ label_wiki: Wiki
+ label_wiki_edit: Editare Wiki
+ label_wiki_edit_plural: Editări Wiki
+ label_wiki_page: Pagină Wiki
+ label_wiki_page_plural: Pagini Wiki
+ label_index_by_title: Sortează după titlu
+ label_index_by_date: Sortează după dată
+ label_current_version: Versiunea curentă
+ label_preview: Previzualizare
+ label_feed_plural: Feed-uri
+ label_changes_details: Detaliile tuturor schimbărilor
+ label_issue_tracking: Urmărire tichete
+ label_spent_time: Timp alocat
+ label_f_hour: "{{value}} oră"
+ label_f_hour_plural: "{{value}} ore"
+ label_time_tracking: Urmărire timp de lucru
+ label_change_plural: Schimbări
+ label_statistics: Statistici
+ label_commits_per_month: Commit pe luna
+ label_commits_per_author: Commit per autor
+ label_view_diff: Afișează diferențele
+ label_diff_inline: în linie
+ label_diff_side_by_side: una lângă alta
+ label_options: Opțiuni
+ label_copy_workflow_from: Copiază modul de lucru de la
+ label_permissions_report: Permisiuni
+ label_watched_issues: Tichete urmărite
+ label_related_issues: Tichete asociate
+ label_applied_status: Stare aplicată
+ label_loading: Încarcă...
+ label_relation_new: Asociere nouă
+ label_relation_delete: Șterge asocierea
+ label_relates_to: asociat cu
+ label_duplicates: duplicate
+ label_duplicated_by: la fel ca
+ label_blocks: blocări
+ label_blocked_by: blocat de
+ label_precedes: precede
+ label_follows: urmează
+ label_end_to_start: de la sfârșit la început
+ label_end_to_end: de la sfârșit la sfârșit
+ label_start_to_start: de la început la început
+ label_start_to_end: de la început la sfârșit
+ label_stay_logged_in: Păstrează autentificarea
+ label_disabled: dezactivat
+ label_show_completed_versions: Arată versiunile terminate
+ label_me: eu
+ label_board: Forum
+ label_board_new: Forum nou
+ label_board_plural: Forumuri
+ label_topic_plural: Subiecte
+ label_message_plural: Mesaje
+ label_message_last: Ultimul mesaj
+ label_message_new: Mesaj nou
+ label_message_posted: Adăugat
+ label_reply_plural: Răspunsuri
+ label_send_information: Trimite utilizatorului informațiile despre cont
+ label_year: An
+ label_month: Lună
+ label_week: Săptămână
+ label_date_from: De la
+ label_date_to: La
+ label_language_based: Un funcție de limba de afișare a utilizatorului
+ label_sort_by: "Sortează după {{value}}"
+ label_send_test_email: Trimite email de test
+ label_feeds_access_key_created_on: "Cheie de acces creată acum {{value}}"
+ label_module_plural: Module
+ label_added_time_by: "Adăugat de {{author}} acum {{age}}"
+ label_updated_time_by: "Actualizat de {{author}} acum {{age}}"
+ label_updated_time: "Actualizat acum {{value}}"
+ label_jump_to_a_project: Alege proiectul...
+ label_file_plural: Fișiere
+ label_changeset_plural: Schimbări
+ label_default_columns: Coloane implicite
+ label_no_change_option: (fără schimbări)
+ label_bulk_edit_selected_issues: Editează toate tichetele selectate
+ label_theme: Tema
+ label_default: Implicită
+ label_search_titles_only: Caută numai în titluri
+ label_user_mail_option_all: "Pentru orice eveniment, în toate proiectele mele"
+ label_user_mail_option_selected: " Pentru orice eveniment, în proiectele selectate..."
+ label_user_mail_option_none: "Doar cele urmărite sau cele în care sunt implicat"
+ label_user_mail_no_self_notified: "Nu trimite notificări pentru modificările mele"
+ label_registration_activation_by_email: activare cont prin email
+ label_registration_manual_activation: activare manuală a contului
+ label_registration_automatic_activation: activare automată a contului
+ label_display_per_page: "pe pagină: {{value}}"
+ label_age: vechime
+ label_change_properties: Schimbă proprietățile
+ label_general: General
+ label_more: Mai mult
+ label_scm: SCM
+ label_plugins: Plugin-uri
+ label_ldap_authentication: autentificare LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Descriere (opțională)
+ label_add_another_file: Adaugă alt fișier
+ label_preferences: Preferințe
+ label_chronological_order: în ordine cronologică
+ label_reverse_chronological_order: În ordine invers cronologică
+ label_planning: Planificare
+ label_incoming_emails: Mesaje primite
+ label_generate_key: Generează o cheie
+ label_issue_watchers: Cine urmărește
+ label_example: Exemplu
+ label_display: Afișează
+
+ label_sort: Sortează
+ label_ascending: Crescător
+ label_descending: Descrescător
+ label_date_from_to: De la {{start}} la {{end}}
+
+
+ button_login: Autentificare
+ button_submit: Trimite
+ button_save: Salvează
+ button_check_all: Bifează tot
+ button_uncheck_all: Debifează tot
+ button_delete: Șterge
+ button_create: Creează
+ button_create_and_continue: Creează și continua
+ button_test: Testează
+ button_edit: Editează
+ button_add: Adaugă
+ button_change: Modifică
+ button_apply: Aplică
+ button_clear: Șterge
+ button_lock: Blochează
+ button_unlock: Deblochează
+ button_download: Descarcă
+ button_list: Listează
+ button_view: Afișează
+ button_move: Mută
+ button_back: Înapoi
+ button_cancel: Anulează
+ button_activate: Activează
+ button_sort: Sortează
+ button_log_time: Înregistrează timpul de lucru
+ button_rollback: Revenire la această versiune
+ button_watch: Urmăresc
+ button_unwatch: Nu urmăresc
+ button_reply: Răspunde
+ button_archive: Arhivează
+ button_unarchive: Dezarhivează
+ button_reset: Resetează
+ button_rename: Redenumește
+ button_change_password: Schimbare parolă
+ button_copy: Copiază
+ button_annotate: Adnotează
+ button_update: Actualizează
+ button_configure: Configurează
+ button_quote: Citează
+
+ status_active: activ
+ status_registered: înregistrat
+ status_locked: blocat
+
+ text_select_mail_notifications: Selectați acțiunile notificate prin email.
+ text_regexp_info: ex. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 înseamnă fără restricții
+ text_project_destroy_confirmation: Sigur doriți să ștergeți proiectul și toate datele asociate?
+ text_subprojects_destroy_warning: "Se vor șterge și sub-proiectele: {{value}}."
+ text_workflow_edit: Selectați un rol și un tip de tichet pentru a edita modul de lucru
+ text_are_you_sure: Sunteți sigur(ă)?
+ text_tip_task_begin_day: sarcină care începe în această zi
+ text_tip_task_end_day: sarcină care se termină în această zi
+ text_tip_task_begin_end_day: sarcină care începe și se termină în această zi
+ text_project_identifier_info: 'Sunt permise doar litere mici (a-z), numere și cratime.<br />Odată salvat, identificatorul nu mai poate fi modificat.'
+ text_caracters_maximum: "maxim {{count}} caractere."
+ text_caracters_minimum: "Trebuie să fie minim {{count}} caractere."
+ text_length_between: "Lungime între {{min}} și {{max}} caractere."
+ text_tracker_no_workflow: Nu sunt moduri de lucru pentru acest tip de tichet
+ text_unallowed_characters: Caractere nepermise
+ text_comma_separated: Sunt permise mai multe valori (separate cu virgulă).
+ text_issues_ref_in_commit_messages: Referire la tichete și rezolvare în textul mesajului
+ text_issue_added: "Tichetul {{id}} a fost adăugat de {{author}}."
+ text_issue_updated: "Tichetul {{id}} a fost actualizat de {{author}}."
+ text_wiki_destroy_confirmation: Sigur doriți ștergerea Wiki și a conținutului asociat?
+ text_issue_category_destroy_question: "Această categorie conține ({{count}}) tichete. Ce doriți să faceți?"
+ text_issue_category_destroy_assignments: Șterge apartenența la categorie.
+ text_issue_category_reassign_to: Atribuie tichetele la această categorie
+ text_user_mail_option: "Pentru proiectele care nu sunt selectate, veți primi notificări doar pentru ceea ce urmăriți sau în ce sunteți implicat (ex: tichete create de dumneavoastră sau care vă sunt atribuite)."
+ text_no_configuration_data: "Nu s-au configurat încă rolurile, stările tichetelor și modurile de lucru.\nEste recomandat să încărcați configurația implicită. O veți putea modifica ulterior."
+ text_load_default_configuration: Încarcă configurația implicită
+ text_status_changed_by_changeset: "Aplicat în setul {{value}}."
+ text_issues_destroy_confirmation: 'Sigur doriți să ștergeți tichetele selectate?'
+ text_select_project_modules: 'Selectați modulele active pentru acest proiect:'
+ text_default_administrator_account_changed: S-a schimbat contul administratorului implicit
+ text_file_repository_writable: Se poate scrie în directorul de atașamente
+ text_plugin_assets_writable: Se poate scrie în directorul de plugin-uri
+ text_rmagick_available: Este disponibil RMagick (opțional)
+ text_destroy_time_entries_question: "{{hours}} ore sunt înregistrate la tichetele pe care doriți să le ștergeți. Ce doriți sa faceți?"
+ text_destroy_time_entries: Șterge orele înregistrate
+ text_assign_time_entries_to_project: Atribuie orele la proiect
+ text_reassign_time_entries: 'Atribuie orele înregistrate la tichetul:'
+ text_user_wrote: "{{value}} a scris:"
+ text_enumeration_destroy_question: "Această valoare are {{count}} obiecte."
+ text_enumeration_category_reassign_to: 'Atribuie la această valoare:'
+ text_email_delivery_not_configured: "Trimiterea de emailuri nu este configurată și ca urmare, notificările sunt dezactivate.\nConfigurați serverul SMTP în config/email.yml și reporniți aplicația pentru a le activa."
+ text_repository_usernames_mapping: "Selectați sau modificați contul Redmine echivalent contului din istoricul depozitului.\nUtilizatorii cu un cont (sau e-mail) identic în Redmine și depozit sunt echivalate automat."
+ text_diff_truncated: '... Comparația a fost trunchiată pentru ca depășește lungimea maximă de text care poate fi afișat.'
+ text_custom_field_possible_values_info: 'O linie pentru fiecare valoare'
+
+ default_role_manager: Manager
+ default_role_developper: Dezvoltator
+ default_role_reporter: Creator de rapoarte
+ default_tracker_bug: Defect
+ default_tracker_feature: Funcție
+ default_tracker_support: Suport
+ default_issue_status_new: Nou
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Rezolvat
+ default_issue_status_feedback: Așteaptă reacții
+ default_issue_status_closed: Închis
+ default_issue_status_rejected: Respins
+ default_doc_category_user: Documentație
+ default_doc_category_tech: Documentație tehnică
+ default_priority_low: mică
+ default_priority_normal: normală
+ default_priority_high: mare
+ default_priority_urgent: urgentă
+ default_priority_immediate: imediată
+ default_activity_design: Design
+ default_activity_development: Dezvoltare
+
+ enumeration_issue_priorities: Priorități tichete
+ enumeration_doc_categories: Categorii documente
+ enumeration_activities: Activități (timp de lucru)
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Această pagină are {{descendants}} pagini anterioare și descendenți. Ce doriți să faceți?
+ text_wiki_page_reassign_children: Atribuie paginile la această pagină
+ text_wiki_page_nullify_children: Menține paginile ca și pagini inițiale (root)
+ text_wiki_page_destroy_children: Șterge paginile și descendenții
+ setting_password_min_length: Lungime minimă parolă
+ field_group_by: Grupează după
+ mail_subject_wiki_content_updated: "Pagina wiki '{{page}}' a fost actualizată"
+ label_wiki_content_added: Adăugat
+ mail_subject_wiki_content_added: "Pagina wiki '{{page}}' a fost adăugată"
+ mail_body_wiki_content_added: Pagina wiki '{{page}}' a fost adăugată de {{author}}.
+ label_wiki_content_updated: Actualizat
+ mail_body_wiki_content_updated: Pagina wiki '{{page}}' a fost actualizată de {{author}}.
+ permission_add_project: Crează proiect
+ setting_new_project_user_role_id: Rol atribuit utilizatorului non-admin care crează un proiect.
+ label_view_all_revisions: Arată toate reviziile
+ label_tag: Tag
+ label_branch: Branch
+ error_no_tracker_in_project: Nu există un tracker asociat cu proiectul. Verificați vă rog setările proiectului.
+ error_no_default_issue_status: Nu există un status implicit al tichetelor. Verificați vă rog configurația (Mergeți la "Administrare -> Stări tichete").
+ text_journal_changed: "{{label}} schimbat din {{old}} în {{new}}"
+ text_journal_set_to: "{{label}} setat ca {{value}}"
+ text_journal_deleted: "{{label}} șters ({{old}})"
+ label_group_plural: Grupuri
+ label_group: Grup
+ label_group_new: Grup nou
+ label_time_entry_plural: Timp alocat
+ 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
--- /dev/null
+# Russian localization for Ruby on Rails 2.2+
+# by Yaroslav Markin <yaroslav@markin.net>
+#
+# 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:
+ 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: 1
+ # 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_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}} лет"
+ 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: "Такая связь приведет к циклической зависимости"
+
+ 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_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: 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_developper: Разработчик
+ 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_t_load_default_data: "Конфигурация по умолчанию не была загружена: {{value}}"
+ error_issue_not_found_in_project: Задача не была найдена или не прикреплена к этому проекту
+ error_scm_annotate: "Данные отсутствуют или не могут быть подписаны."
+ error_scm_command_failed: "Ошибка доступа к хранилищу: {{value}}"
+ error_scm_not_found: Хранилище не содержит записи и/или исправления.
+
+ field_account: Учетная запись
+ field_activity: Деятельность
+ field_admin: Администратор
+ field_assignable: Задача может быть назначена этой роли
+ field_assigned_to: Назначена
+ field_attr_firstname: Имя
+ field_attr_lastname: Фамилия
+ field_attr_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_chlog: Задачи, отображаемые в журнале изменений
+ 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_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_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: 'Да'
+
+ gui_validation_error: 1 ошибка
+ gui_validation_error_plural: "{{count}} ошибок"
+ gui_validation_error_plural2: "{{count}} ошибки"
+ gui_validation_error_plural5: "{{count}} ошибок"
+
+ 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_log: Журнал изменений
+ 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_download: "{{count}} загрузка"
+ label_download_plural: "{{count}} скачиваний"
+ label_download_plural2: "{{count}} загрузки"
+ label_download_plural5: "{{count}} загрузок"
+ 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: RSS
+ label_feeds_access_key_created_on: "Ключ доступа RSS создан {{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_modification: "{{count}} изменение"
+ label_modification_plural: "{{count}} изменений"
+ label_modification_plural2: "{{count}} изменения"
+ label_modification_plural5: "{{count}} изменений"
+ label_modified: изменено
+ label_module_plural: Модули
+ label_months_from: месяцев(ца) с
+ label_month: Месяц
+ label_more_than_ago: более, чем дней(я) назад
+ label_more: Больше
+ label_my_account: Моя учетная запись
+ label_my_page: Моя страница
+ 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_project: проект
+ label_project_all: Все проекты
+ 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_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_none: "Только о тех событиях, которые я отслеживаю или в которых я участвую"
+ label_user_mail_option_selected: "О всех событиях только в выбранном проекте..."
+ 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: 1 закрыт
+ other: "{{count}} закрыто"
+ label_x_comments:
+ zero: нет комментариев
+ one: 1 комментарий
+ other: "{{count}} комментариев"
+ label_x_open_issues_abbr:
+ zero: 0 открыто
+ one: 1 открыт
+ other: "{{count}} открыто"
+ label_x_open_issues_abbr_on_total:
+ zero: 0 открыто / {{total}}
+ one: 1 открыт / {{total}}
+ other: "{{count}} открыто / {{total}}"
+ label_x_projects:
+ zero: нет проектов
+ one: 1 проект
+ 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}} назначенных на Вас задач в ближайшие дни"
+
+ 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_feeds_access_key_reseted: Ваш ключ доступа RSS был перезапущен.
+ 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_log_time: Учет затраченного времени
+ permission_view_changesets: Просмотр изменений хранилища
+ permission_view_time_entries: Просмотр затраченного времени
+ permission_manage_boards: Управление форумами
+ permission_manage_categories: Управление категориями задач
+ permission_manage_documents: Управление документами
+ permission_manage_files: Управление файлами
+ permission_manage_issue_relations: Управление связыванием задач
+ permission_manage_members: Управление участниками
+ permission_manage_news: Управление новостями
+ permission_manage_public_queries: Управление общими запросами
+ permission_manage_repository: Управление хранилищем
+ 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
+
+ setting_activity_days_default: Количество дней, отображаемых в Активности
+ setting_app_subtitle: Подзаголовок приложения
+ setting_app_title: Название приложения
+ setting_attachment_max_size: Максимальный размер вложения
+ setting_autofetch_changesets: Автоматически следить за изменениями хранилища
+ setting_autologin: Автоматический вход
+ setting_bcc_recipients: Использовать скрытые списки (bcc)
+ setting_commit_fix_keywords: Назначение ключевых слов
+ setting_commit_logs_encoding: Кодировка комментариев в хранилище
+ setting_commit_ref_keywords: Ключевые слова для поиска
+ setting_cross_project_issue_relations: Разрешить пересечение задач по проектам
+ setting_date_format: Формат даты
+ setting_default_language: Язык по умолчанию
+ setting_default_projects_public: Новые проекты являются общедоступными
+ setting_diff_max_lines_displayed: Максимальное число строк для diff
+ setting_display_subprojects_issues: Отображение подпроектов по умолчанию
+ setting_emails_footer: Подстрочные примечания Email
+ setting_enabled_scm: Разрешенные SCM
+ setting_feeds_limit: Ограничение количества заголовков для RSS потока
+ 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_repositories_encodings: Кодировки хранилища
+ 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/email.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_project_identifier_info: 'Допустимы строчные буквы (a-z), цифры и дефис.<br />Сохраненный идентификатор не может быть изменен.'
+ 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_task_begin_day: дата начала задачи
+ text_tip_task_begin_end_day: начало задачи и окончание ее в этот день
+ text_tip_task_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-страница '{{page}}' была обновлена"
+ label_wiki_content_added: Добавлена wiki-страница
+ mail_subject_wiki_content_added: "Wiki-страница '{{page}}' была добавлена"
+ mail_body_wiki_content_added: "{{author}} добавил(а) wiki-страницу '{{page}}'."
+ label_wiki_content_updated: Обновлена wiki-страница
+ mail_body_wiki_content_updated: "{{author}} обновил(а) wiki-страницу '{{page}}'."
+ 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 по умолчанию
--- /dev/null
+sk:
+ 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: [Nedeľa, Pondelok, Utorok, Streda, Štvrtok, Piatok, Sobota]
+ abbr_day_names: [Ne, Po, Ut, St, Št, Pi, So]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, Január, Február, Marec, Apríl, Máj, Jún, Júl, August, September, Október, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, Máj, Jún, Júl, Aug, Sep, Okt, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "pol minúty"
+ less_than_x_seconds:
+ one: "menej ako 1 sekunda"
+ other: "menej ako {{count}} sekúnd"
+ x_seconds:
+ one: "1 sekunda"
+ other: "{{count}} sekúnd"
+ less_than_x_minutes:
+ one: "menej ako minúta"
+ other: "menej ako {{count}} minút"
+ x_minutes:
+ one: "1 minuta"
+ other: "{{count}} minút"
+ about_x_hours:
+ one: "okolo 1 hodiny"
+ other: "okolo {{count}} hodín"
+ x_days:
+ one: "1 deň"
+ other: "{{count}} dní"
+ about_x_months:
+ one: "okolo 1 mesiaca"
+ other: "okolo {{count}} mesiace/ov"
+ x_months:
+ one: "1 mesiac"
+ other: "{{count}} mesiace/ov"
+ about_x_years:
+ one: "okolo 1 roka"
+ other: "okolo {{count}} roky/ov"
+ over_x_years:
+ one: "cez 1 rok"
+ other: "cez {{count}} roky/ov"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "a"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "nieje zahrnuté v zozname"
+ exclusion: "je rezervované"
+ invalid: "je neplatné"
+ confirmation: "sa nezhoduje s potvrdením"
+ accepted: "musí byť akceptované"
+ empty: "nemôže byť prázdne"
+ blank: "nemôže byť prázdne"
+ too_long: "je príliš dlhé"
+ too_short: "je príliš krátke"
+ wrong_length: "má chybnú dĺžku"
+ taken: "je už použité"
+ not_a_number: "nieje číslo"
+ not_a_date: "nieje platný dátum"
+ greater_than: "musí byť väčšíe ako {{count}}"
+ greater_than_or_equal_to: "musí byť väčšie alebo rovné {{count}}"
+ equal_to: "musí byť rovné {{count}}"
+ less_than: "musí byť menej ako {{count}}"
+ less_than_or_equal_to: "musí byť menej alebo rovné {{count}}"
+ odd: "musí byť nepárne"
+ even: "musí byť párne"
+ greater_than_start_date: "musí byť neskôr ako počiatočný dátum"
+ not_same_project: "nepatrí rovnakému projektu"
+ circular_dependency: "Tento vzťah by vytvoril cyklickú závislosť"
+
+ # SK translation by Stanislav Pach | stano.pach@seznam.cz
+
+ actionview_instancetag_blank_option: Prosím vyberte
+
+ general_text_No: 'Nie'
+ general_text_Yes: 'Áno'
+ general_text_no: 'nie'
+ general_text_yes: 'áno'
+ general_lang_name: 'Slovenčina'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: UTF-8
+ general_pdf_encoding: UTF-8
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Účet bol úspešne zmenený.
+ notice_account_invalid_creditentials: Chybné meno alebo heslo
+ notice_account_password_updated: Heslo bolo úspešne zmenené.
+ notice_account_wrong_password: Chybné heslo
+ notice_account_register_done: Účet bol úspešne vytvorený. Pre aktiváciu účtu kliknite na odkaz v emailu, ktorý vam bol zaslaný.
+ notice_account_unknown_email: Neznámy užívateľ.
+ notice_can_t_change_password: Tento účet používa externú autentifikáciu. Tu heslo zmeniť nemôžete.
+ notice_account_lost_email_sent: Bol vám zaslaný email s inštrukciami ako si nastavite nové heslo.
+ notice_account_activated: Váš účet bol aktivovaný. Teraz se môžete prihlásiť.
+ notice_successful_create: Úspešne vytvorené.
+ notice_successful_update: Úspešne aktualizované.
+ notice_successful_delete: Úspešne odstránené.
+ notice_successful_connection: Úspešne pripojené.
+ notice_file_not_found: Stránka, ktorú se snažíte zobraziť, neexistuje alebo bola zmazaná.
+ notice_locking_conflict: Údaje boli zmenené iným užívateľom.
+ notice_scm_error: Položka a/alebo revízia neexistuje v repozitári.
+ notice_not_authorized: Nemáte dostatočné práva pre zobrazenie tejto stránky.
+ notice_email_sent: "Na adresu {{value}} bol odeslaný email"
+ notice_email_error: "Pri odosielaní emailu nastala chyba ({{value}})"
+ notice_feeds_access_key_reseted: Váš klúč pre prístup k Atomu bol resetovaný.
+ notice_failed_to_save_issues: "Nastala chyba pri ukládaní {{count}} úloh na {{total}} zvolený: {{ids}}."
+ notice_no_issue_selected: "Nebola zvolená žiadná úloha. Prosím, zvoľte úlohy, ktoré chcete upraviť"
+ notice_account_pending: "Váš účet bol vytvorený, teraz čaká na schválenie administrátorom."
+ notice_default_data_loaded: Výchozia konfigurácia úspešne nahraná.
+
+ error_can_t_load_default_data: "Výchozia konfigurácia nebola nahraná: {{value}}"
+ error_scm_not_found: "Položka a/alebo revízia neexistuje v repozitári."
+ error_scm_command_failed: "Pri pokuse o prístup k repozitári došlo k chybe: {{value}}"
+ error_issue_not_found_in_project: 'Úloha nebola nájdená alebo nepatrí k tomuto projektu'
+
+ mail_subject_lost_password: "Vaše heslo ({{value}})"
+ mail_body_lost_password: 'Pre zmenu vašeho hesla kliknite na následujúci odkaz:'
+ mail_subject_register: "Aktivácia účtu ({{value}})"
+ mail_body_register: 'Pre aktiváciu vašeho účtu kliknite na následujúci odkaz:'
+ mail_body_account_information_external: "Pomocou vašeho účtu {{value}} se môžete prihlásiť."
+ mail_body_account_information: Informácie o vašom účte
+ mail_subject_account_activation_request: "Aktivácia {{value}} účtu"
+ mail_body_account_activation_request: "Bol zaregistrovaný nový uživateľ {{value}}. Aktivácia jeho účtu závisí na vašom potvrdení."
+
+ gui_validation_error: 1 chyba
+ gui_validation_error_plural: "{{count}} chyb(y)"
+
+ field_name: Názov
+ field_description: Popis
+ field_summary: Prehľad
+ field_is_required: Povinné pole
+ field_firstname: Meno
+ field_lastname: Priezvisko
+ field_mail: Email
+ field_filename: Súbor
+ field_filesize: Veľkosť
+ field_downloads: Stiahnuté
+ field_author: Autor
+ field_created_on: Vytvorené
+ field_updated_on: Aktualizované
+ field_field_format: Formát
+ field_is_for_all: Pre všetky projekty
+ field_possible_values: Možné hodnoty
+ field_regexp: Regulérny výraz
+ field_min_length: Minimálna dĺžka
+ field_max_length: Maximálna dĺžka
+ field_value: Hodnota
+ field_category: Kategória
+ field_title: Názov
+ field_project: Projekt
+ field_issue: Úloha
+ field_status: Stav
+ field_notes: Poznámka
+ field_is_closed: Úloha uzavretá
+ field_is_default: Východzí stav
+ field_tracker: Fronta
+ field_subject: Predmet
+ field_due_date: Uzavrieť do
+ field_assigned_to: Priradené
+ field_priority: Priorita
+ field_fixed_version: Priradené k verzii
+ field_user: Užívateľ
+ field_role: Rola
+ field_homepage: Domovská stránka
+ field_is_public: Verejný
+ field_parent: Nadradený projekt
+ field_is_in_chlog: Úlohy zobrazené v rozdielovom logu
+ field_is_in_roadmap: Úlohy zobrazené v pláne
+ field_login: Login
+ field_mail_notification: Emailové oznámenie
+ field_admin: Administrátor
+ field_last_login_on: Posledné prihlásenie
+ field_language: Jazyk
+ field_effective_date: Dátum
+ field_password: Heslo
+ field_new_password: Nové heslo
+ field_password_confirmation: Potvrdenie
+ field_version: Verzia
+ field_type: Typ
+ field_host: Host
+ field_port: Port
+ field_account: Účet
+ field_base_dn: Base DN
+ field_attr_login: Prihlásenie (atribut)
+ field_attr_firstname: Meno (atribut)
+ field_attr_lastname: Priezvisko (atribut)
+ field_attr_mail: Email (atribut)
+ field_onthefly: Automatické vytváranie užívateľov
+ field_start_date: Začiatok
+ field_done_ratio: % hotovo
+ field_auth_source: Autentifikačný mód
+ field_hide_mail: Nezobrazovať môj email
+ field_comments: Komentár
+ field_url: URL
+ field_start_page: Výchozia stránka
+ field_subproject: Podprojekt
+ field_hours: Hodiny
+ field_activity: Aktivita
+ field_spent_on: Dátum
+ field_identifier: Identifikátor
+ field_is_filter: Použiť ako filter
+ field_issue_to: Súvisiaca úloha
+ field_delay: Oneskorenie
+ field_assignable: Úlohy môžu byť priradené tejto roli
+ field_redirect_existing_links: Presmerovať existujúce odkazy
+ field_estimated_hours: Odhadovaná doba
+ field_column_names: Stĺpce
+ field_time_zone: Časové pásmo
+ field_searchable: Umožniť vyhľadávanie
+ field_default_value: Východzia hodnota
+ field_comments_sorting: Zobraziť komentáre
+
+ setting_app_title: Názov aplikácie
+ setting_app_subtitle: Podtitulok aplikácie
+ setting_welcome_text: Uvítací text
+ setting_default_language: Východzí jazyk
+ setting_login_required: Auten. vyžadovaná
+ setting_self_registration: Povolenie registrácie
+ setting_attachment_max_size: Maximálna veľkosť prílohy
+ setting_issues_export_limit: Limit pre export úloh
+ setting_mail_from: Odosielať emaily z adresy
+ setting_bcc_recipients: Príjemcovia skrytej kópie (bcc)
+ setting_host_name: Hostname
+ setting_text_formatting: Formátovanie textu
+ setting_wiki_compression: Kompresia histórie Wiki
+ setting_feeds_limit: Limit zobrazených položiek (Atom feed)
+ setting_default_projects_public: Nové projekty nastavovať ako verejné
+ setting_autofetch_changesets: Automatický prenos zmien
+ setting_sys_api_enabled: Povolit Webovú Službu (WS) pre správu repozitára
+ setting_commit_ref_keywords: Klúčové slová pre odkazy
+ setting_commit_fix_keywords: Klúčové slová pre uzavretie
+ setting_autologin: Automatické prihlasovanie
+ setting_date_format: Formát dátumu
+ setting_time_format: Formát času
+ setting_cross_project_issue_relations: Povoliť väzby úloh skrz projekty
+ setting_issue_list_default_columns: Východzie stĺpce zobrazené v zozname úloh
+ setting_ itories_encodings: Kódovanie
+ setting_emails_footer: Zapätie emailov
+ setting_protocol: Protokol
+ setting_per_page_options: Povolené množstvo riadkov na stránke
+ setting_user_format: Formát zobrazenia užívateľa
+ setting_activity_days_default: "Zobrazené dni aktivity projektu:"
+ setting_display_subprojects_issues: Prednastavenie zobrazenia úloh podporojektov v hlavnom projekte
+
+ project_module_issue_tracking: Sledovanie úloh
+ project_module_time_tracking: Sledovanie času
+ project_module_news: Novinky
+ project_module_documents: Dokumenty
+ project_module_files: Súbory
+ project_module_wiki: Wiki
+ project_module_repository: Repozitár
+ project_module_boards: Diskusie
+
+ label_user: Užívateľ
+ label_user_plural: Užívatelia
+ label_user_new: Nový užívateľ
+ label_project: Projekt
+ label_project_new: Nový projekt
+ label_project_plural: Projekty
+ label_x_projects:
+ zero: žiadne projekty
+ one: 1 projekt
+ other: "{{count}} projekty/ov"
+ label_project_all: Všetky projekty
+ label_project_latest: Posledné projekty
+ label_issue: Úloha
+ label_issue_new: Nová úloha
+ label_issue_plural: Úlohy
+ label_issue_view_all: Všetky úlohy
+ label_issues_by: "Úlohy od užívateľa {{value}}"
+ label_issue_added: Úloha pridaná
+ label_issue_updated: Úloha aktualizovaná
+ label_document: Dokument
+ label_document_new: Nový dokument
+ label_document_plural: Dokumenty
+ label_document_added: Dokument pridaný
+ label_role: Rola
+ label_role_plural: Role
+ label_role_new: Nová rola
+ label_role_and_permissions: Role a práva
+ label_member: Člen
+ label_member_new: Nový člen
+ label_member_plural: Členovia
+ label_tracker: Fronta
+ label_tracker_plural: Fronty
+ label_tracker_new: Nová fronta
+ label_workflow: Workflow
+ label_issue_status: Stav úloh
+ label_issue_status_plural: Stavy úloh
+ label_issue_status_new: Nový stav
+ label_issue_category: Kategória úloh
+ label_issue_category_plural: Kategórie úloh
+ label_issue_category_new: Nová kategória
+ label_custom_field: Užívateľské pole
+ label_custom_field_plural: Užívateľské polia
+ label_custom_field_new: Nové užívateľské pole
+ label_enumerations: Zoznamy
+ label_enumeration_new: Nová hodnota
+ label_information: Informácia
+ label_information_plural: Informácie
+ label_please_login: Prosím prihláste sa
+ label_register: Registrovať
+ label_password_lost: Zabudnuté heslo
+ label_home: Domovská stránka
+ label_my_page: Moja stránka
+ label_my_account: Môj účet
+ label_my_projects: Moje projekty
+ label_administration: Administrácia
+ label_login: Prihlásenie
+ label_logout: Odhlásenie
+ label_help: Nápoveda
+ label_reported_issues: Nahlásené úlohy
+ label_assigned_to_me_issues: Moje úlohy
+ label_last_login: Posledné prihlásenie
+ label_registered_on: Registrovaný
+ label_activity: Aktivita
+ label_overall_activity: Celková aktivita
+ label_new: Nový
+ label_logged_as: Prihlásený ako
+ label_environment: Prostredie
+ label_authentication: Autentifikácia
+ label_auth_source: Mód autentifikácie
+ label_auth_source_new: Nový mód autentifikácie
+ label_auth_source_plural: Módy autentifikácie
+ label_subproject_plural: Podprojekty
+ label_min_max_length: Min - Max dĺžka
+ label_list: Zoznam
+ label_date: Dátum
+ label_integer: Celé číslo
+ label_float: Desatinné číslo
+ label_boolean: Áno/Nie
+ label_string: Text
+ label_text: Dlhý text
+ label_attribute: Atribut
+ label_attribute_plural: Atributy
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloady"
+ label_no_data: Žiadné položky
+ label_change_status: Zmeniť stav
+ label_history: História
+ label_attachment: Súbor
+ label_attachment_new: Nový súbor
+ label_attachment_delete: Odstrániť súbor
+ label_attachment_plural: Súbory
+ label_file_added: Súbor pridaný
+ label_report: Prehľad
+ label_report_plural: Prehľady
+ label_news: Novinky
+ label_news_new: Pridať novinku
+ label_news_plural: Novinky
+ label_news_latest: Posledné novinky
+ label_news_view_all: Zobrazit všetky novinky
+ label_news_added: Novinka pridaná
+ label_change_log: Protokol zmien
+ label_settings: Nastavenie
+ label_overview: Prehľad
+ label_version: Verzia
+ label_version_new: Nová verzia
+ label_version_plural: Verzie
+ label_confirmation: Potvrdenie
+ label_export_to: 'Tiež k dispozícií:'
+ label_read: Načíta sa...
+ label_public_projects: Verejné projekty
+ label_open_issues: Otvorený
+ label_open_issues_plural: Otvorené
+ label_closed_issues: Uzavrený
+ label_closed_issues_plural: Uzavrené
+ label_x_open_issues_abbr_on_total:
+ zero: 0 otvorených z celkovo {{total}}
+ one: 1 otvorený z celkovo {{total}}
+ other: "{{count}} otvorené/ých z celkovo {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 otvorených
+ one: 1 otvorený
+ other: "{{count}} otvorené/ých"
+ label_x_closed_issues_abbr:
+ zero: 0 zavretých
+ one: 1 zavretý
+ other: "{{count}} zavreté/ých"
+ label_total: Celkovo
+ label_permissions: Práva
+ label_current_status: Aktuálny stav
+ label_new_statuses_allowed: Nové povolené stavy
+ label_all: všetko
+ label_none: nič
+ label_nobody: nikto
+ label_next: Ďalší
+ label_previous: Predchádzajúci
+ label_used_by: Použité
+ label_details: Detaily
+ label_add_note: Pridať poznámku
+ label_per_page: Na stránku
+ label_calendar: Kalendár
+ label_months_from: mesiacov od
+ label_gantt: Ganttov graf
+ label_internal: Interný
+ label_last_changes: "posledných {{count}} zmien"
+ label_change_view_all: Zobraziť všetky zmeny
+ label_personalize_page: Prispôsobiť túto stránku
+ label_comment: Komentár
+ label_comment_plural: Komentáre
+ label_x_comments:
+ zero: žiaden komentár
+ one: 1 komentár
+ other: "{{count}} komentáre/ov"
+ label_comment_add: Pridať komentár
+ label_comment_added: Komentár pridaný
+ label_comment_delete: Odstrániť komentár
+ label_query: Užívateľský dotaz
+ label_query_plural: Užívateľské dotazy
+ label_query_new: Nový dotaz
+ label_filter_add: Pridať filter
+ label_filter_plural: Filtre
+ label_equals: je
+ label_not_equals: nieje
+ label_in_less_than: je menší ako
+ label_in_more_than: je väčší ako
+ label_in: v
+ label_today: dnes
+ label_all_time: vždy
+ label_yesterday: včera
+ label_this_week: tento týždeň
+ label_last_week: minulý týždeň
+ label_last_n_days: "posledných {{count}} dní"
+ label_this_month: tento mesiac
+ label_last_month: minulý mesiac
+ label_this_year: tento rok
+ label_date_range: Časový rozsah
+ label_less_than_ago: pred menej ako (dňami)
+ label_more_than_ago: pred viac ako (dňami)
+ label_ago: pred (dňami)
+ label_contains: obsahuje
+ label_not_contains: neobsahuje
+ label_day_plural: dní
+ label_repository: Repozitár
+ label_repository_plural: Repozitáre
+ label_browse: Prechádzať
+ label_modification: "{{count}} zmena"
+ label_modification_plural: "{{count}} zmien"
+ label_revision: Revízia
+ label_revision_plural: Revízií
+ label_associated_revisions: Súvisiace verzie
+ label_added: pridané
+ label_modified: zmenené
+ label_deleted: odstránené
+ label_latest_revision: Posledná revízia
+ label_latest_revision_plural: Posledné revízie
+ label_view_revisions: Zobraziť revízie
+ label_max_size: Maximálna veľkosť
+ label_sort_highest: Presunúť na začiatok
+ label_sort_higher: Presunúť navrch
+ label_sort_lower: Presunúť dole
+ label_sort_lowest: Presunúť na koniec
+ label_roadmap: Plán
+ label_roadmap_due_in: "Zostáva {{value}}"
+ label_roadmap_overdue: "{{value}} neskoro"
+ label_roadmap_no_issues: Pre túto verziu niesú žiadne úlohy
+ label_search: Hľadať
+ label_result_plural: Výsledky
+ label_all_words: Všetky slova
+ label_wiki: Wiki
+ label_wiki_edit: Wiki úprava
+ label_wiki_edit_plural: Wiki úpravy
+ label_wiki_page: Wiki stránka
+ label_wiki_page_plural: Wiki stránky
+ label_index_by_title: Index podľa názvu
+ label_index_by_date: Index podľa dátumu
+ label_current_version: Aktuálna verzia
+ label_preview: Náhľad
+ label_feed_plural: Príspevky
+ label_changes_details: Detail všetkých zmien
+ label_issue_tracking: Sledovanie úloh
+ label_spent_time: Strávený čas
+ label_f_hour: "{{value}} hodina"
+ label_f_hour_plural: "{{value}} hodín"
+ label_time_tracking: Sledovánie času
+ label_change_plural: Zmeny
+ label_statistics: Štatistiky
+ label_commits_per_month: Úkony za mesiac
+ label_commits_per_author: Úkony podľa autora
+ label_view_diff: Zobrazit rozdiely
+ label_diff_inline: vo vnútri
+ label_diff_side_by_side: vedľa seba
+ label_options: Nastavenie
+ label_copy_workflow_from: Kopírovať workflow z
+ label_permissions_report: Prehľad práv
+ label_watched_issues: Sledované úlohy
+ label_related_issues: Súvisiace úlohy
+ label_applied_status: Použitý stav
+ label_loading: Nahrávam ...
+ label_relation_new: Nová súvislosť
+ label_relation_delete: Odstrániť súvislosť
+ label_relates_to: súvisiací s
+ label_duplicates: duplicity
+ label_blocks: blokovaný
+ label_blocked_by: zablokovaný
+ label_precedes: predcháza
+ label_follows: následuje
+ label_end_to_start: od konca na začiatok
+ label_end_to_end: od konca do konca
+ label_start_to_start: od začiatku do začiatku
+ label_start_to_end: od začiatku do konca
+ label_stay_logged_in: Zostať prihlásený
+ label_disabled: zakazané
+ label_show_completed_versions: Ukázať dokončené verzie
+ label_me: ja
+ label_board: Fórum
+ label_board_new: Nové fórum
+ label_board_plural: Fóra
+ label_topic_plural: Témy
+ label_message_plural: Správy
+ label_message_last: Posledná správa
+ label_message_new: Nová správa
+ label_message_posted: Správa pridaná
+ label_reply_plural: Odpovede
+ label_send_information: Zaslať informácie o účte užívateľa
+ label_year: Rok
+ label_month: Mesiac
+ label_week: Týžden
+ label_date_from: Od
+ label_date_to: Do
+ label_language_based: Podľa výchozieho jazyka
+ label_sort_by: "Zoradenie podľa {{value}}"
+ label_send_test_email: Poslať testovací email
+ label_feeds_access_key_created_on: "Prístupový klúč pre RSS bol vytvorený pred {{value}}"
+ label_module_plural: Moduly
+ label_added_time_by: "Pridané užívateľom {{author}} pred {{age}}"
+ label_updated_time: "Aktualizované pred {{value}}"
+ label_jump_to_a_project: Zvoliť projekt...
+ label_file_plural: Súbory
+ label_changeset_plural: Sady zmien
+ label_default_columns: Východzie stĺpce
+ label_no_change_option: (bez zmeny)
+ label_bulk_edit_selected_issues: Skupinová úprava vybraných úloh
+ label_theme: Téma
+ label_default: Východzí
+ label_search_titles_only: Vyhľadávať iba v názvoch
+ label_user_mail_option_all: "Pre všetky události všetkých mojích projektov"
+ label_user_mail_option_selected: "Pre všetky události vybraných projektov"
+ label_user_mail_option_none: "Len pre události, ktoré sledujem alebo sa ma týkajú"
+ label_user_mail_no_self_notified: "Nezasielať informácie o mnou vytvorených zmenách"
+ label_registration_activation_by_email: aktivácia účtu emailom
+ label_registration_manual_activation: manuálna aktivácia účtu
+ label_registration_automatic_activation: automatická aktivácia účtu
+ label_display_per_page: "{{value}} na stránku"
+ label_age: Vek
+ label_change_properties: Zmeniť vlastnosti
+ label_general: Všeobecné
+ label_more: Viac
+ label_scm: SCM
+ label_plugins: Pluginy
+ label_ldap_authentication: Autentifikácia LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: Voliteľný popis
+ label_add_another_file: Pridať ďaľší súbor
+ label_preferences: Nastavenia
+ label_chronological_order: V chronologickom poradí
+ label_reverse_chronological_order: V obrátenom chronologickom poradí
+
+ button_login: Prihlásiť
+ button_submit: Potvrdiť
+ button_save: Uložiť
+ button_check_all: Označiť všetko
+ button_uncheck_all: Odznačiť všetko
+ button_delete: Odstrániť
+ button_create: Vytvoriť
+ button_test: Test
+ button_edit: Upraviť
+ button_add: Pridať
+ button_change: Zmeniť
+ button_apply: Použiť
+ button_clear: Zmazať
+ button_lock: Zamknúť
+ button_unlock: Odomknúť
+ button_download: Stiahnúť
+ button_list: Vypísať
+ button_view: Zobraziť
+ button_move: Presunúť
+ button_back: Naspäť
+ button_cancel: Storno
+ button_activate: Aktivovať
+ button_sort: Zoradenie
+ button_log_time: Pridať čas
+ button_rollback: Naspäť k tejto verzii
+ button_watch: Sledovať
+ button_unwatch: Nesledovať
+ button_reply: Odpovedať
+ button_archive: Archivovať
+ button_unarchive: Odarchivovať
+ button_reset: Reset
+ button_rename: Premenovať
+ button_change_password: Zmeniť heslo
+ button_copy: Kopírovať
+ button_annotate: Komentovať
+ button_update: Aktualizovať
+ button_configure: Konfigurovať
+
+ status_active: aktívny
+ status_registered: registrovaný
+ status_locked: uzamknutý
+
+ text_select_mail_notifications: Vyberte akciu, pri ktorej bude zaslané upozornenie emailom
+ text_regexp_info: napr. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 znamená bez limitu
+ text_project_destroy_confirmation: Ste si istý, že chcete odstránit tento projekt a všetky súvisiace dáta ?
+ text_workflow_edit: Vyberte rolu a frontu k úprave workflow
+ text_are_you_sure: Ste si istý?
+ text_tip_task_begin_day: úloha začína v tento deň
+ text_tip_task_end_day: úloha končí v tento deň
+ text_tip_task_begin_end_day: úloha začína a končí v tento deň
+ text_project_identifier_info: 'Povolené znaky sú malé písmena (a-z), čísla a pomlčka.<br />Po uložení už nieje možné identifikátor zmeniť.'
+ text_caracters_maximum: "{{count}} znakov maximálne."
+ text_caracters_minimum: "Musí byť aspoň {{count}} znaky/ov dlhé."
+ text_length_between: "Dĺžka medzi {{min}} až {{max}} znakmi."
+ text_tracker_no_workflow: Pre tuto frontu nieje definovaný žiadný workflow
+ text_unallowed_characters: Nepovolené znaky
+ text_comma_separated: Je povolené viacero hodnôt (oddelené navzájom čiarkou).
+ text_issues_ref_in_commit_messages: Odkazovať a upravovať úlohy v správach s následovnym obsahom
+ text_issue_added: "úloha {{id}} bola vytvorená užívateľom {{author}}."
+ text_issue_updated: "Úloha {{id}} byla aktualizovaná užívateľom {{author}}."
+ text_wiki_destroy_confirmation: Naozaj si prajete odstrániť túto Wiki a celý jej obsah?
+ text_issue_category_destroy_question: "Niektoré úlohy ({{count}}) sú priradené k tejto kategórii. Čo chtete s nimi spraviť?"
+ text_issue_category_destroy_assignments: Zrušiť priradenie ku kategórii
+ text_issue_category_reassign_to: Priradiť úlohy do tejto kategórie
+ text_user_mail_option: "U projektov, které neboli vybrané, budete dostávať oznamenie iba o vašich či o sledovaných položkách (napr. o položkách, ktorých ste autor, alebo ku ktorým ste priradený/á)."
+ text_no_configuration_data: "Role, fronty, stavy úloh ani workflow neboli zatiaľ nakonfigurované.\nVelmi doporučujeme nahrať východziu konfiguráciu. Potom si môžete všetko upraviť"
+ text_load_default_configuration: Nahrať východziu konfiguráciu
+ text_status_changed_by_changeset: "Aktualizované v sade zmien {{value}}."
+ text_issues_destroy_confirmation: 'Naozaj si prajete odstrániť všetky zvolené úlohy?'
+ text_select_project_modules: 'Aktivne moduly v tomto projekte:'
+ text_default_administrator_account_changed: Zmenené výchozie nastavenie administrátorského účtu
+ text_file_repository_writable: Povolený zápis do repozitára
+ text_rmagick_available: RMagick k dispozícií (voliteľné)
+ text_destroy_time_entries_question: U úloh, které chcete odstraniť, je evidované %.02f práce. Čo chcete vykonať?
+ text_destroy_time_entries: Odstrániť evidované hodiny.
+ text_assign_time_entries_to_project: Priradiť evidované hodiny projektu
+ text_reassign_time_entries: 'Preradiť evidované hodiny k tejto úlohe:'
+
+ default_role_manager: Manažér
+ default_role_developper: Vývojár
+ default_role_reporter: Reportér
+ default_tracker_bug: Chyba
+ default_tracker_feature: Rozšírenie
+ default_tracker_support: Podpora
+ default_issue_status_new: Nový
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Vyriešený
+ default_issue_status_feedback: Čaká sa
+ default_issue_status_closed: Uzavrený
+ default_issue_status_rejected: Odmietnutý
+ default_doc_category_user: Užívateľská dokumentácia
+ default_doc_category_tech: Technická dokumentácia
+ default_priority_low: Nízká
+ default_priority_normal: Normálna
+ default_priority_high: Vysoká
+ default_priority_urgent: Urgentná
+ default_priority_immediate: Okamžitá
+ default_activity_design: Design
+ default_activity_development: Vývoj
+
+ enumeration_issue_priorities: Priority úloh
+ enumeration_doc_categories: Kategorie dokumentov
+ enumeration_activities: Aktivity (sledovanie času)
+ error_scm_annotate: "Položka neexistuje alebo nemôže byť komentovaná."
+ label_planning: Plánovanie
+ text_subprojects_destroy_warning: "Jeho podprojekt(y): {{value}} budú takisto vymazané."
+ label_and_its_subprojects: "{{value}} a jeho podprojekty"
+ mail_body_reminder: "{{count}} úloha(y), ktorá(é) je(sú) vám priradený(é), ma(jú) byť hotova(é) za {{days}} dní:"
+ mail_subject_reminder: "{{count}} úloha(y) ma(jú) byť hotova(é) za pár dní"
+ text_user_wrote: "{{value}} napísal:"
+ label_duplicated_by: duplikovaný
+ setting_enabled_scm: Zapnúť SCM
+ text_enumeration_category_reassign_to: 'Prenastaviť na túto hodnotu:'
+ text_enumeration_destroy_question: "{{count}} objekty sú nastavené na túto hodnotu."
+ label_incoming_emails: Príchádzajúce emaily
+ label_generate_key: Vygenerovať kľúč
+ setting_mail_handler_api_enabled: Zapnúť Webovú Službu (WS) pre príchodzie emaily
+ setting_mail_handler_api_key: API kľúč
+ text_email_delivery_not_configured: "Doručenie emailov nieje nastavené, notifikácie sú vypnuté.\nNastavte váš SMTP server v config/email.yml a reštartnite aplikáciu pre aktiváciu funkcie."
+ field_parent_title: Nadradená stránka
+ label_issue_watchers: Pozorovatelia
+ setting_commit_logs_encoding: Kódovanie prenášaných správ
+ button_quote: Citácia
+ setting_sequential_project_identifiers: Generovať sekvenčné identifikátory projektov
+ notice_unable_delete_version: Verzia nemôže byť zmazaná
+ label_renamed: premenované
+ label_copied: kopírované
+ setting_plain_text_mail: Len jednoduchý text (bez HTML)
+ permission_view_files: Zobrazenie súborov
+ permission_edit_issues: Úprava úloh
+ permission_edit_own_time_entries: Úprava vlastných zaznamov o strávenom čase
+ permission_manage_public_queries: Správa verejných otáziek
+ permission_add_issues: Pridanie úlohy
+ permission_log_time: Zaznamenávanie stráveného času
+ permission_view_changesets: Zobrazenie sád zmien
+ permission_view_time_entries: Zobrazenie stráveného času
+ permission_manage_versions: Správa verzií
+ permission_manage_wiki: Správa Wiki
+ permission_manage_categories: Správa kategórií úloh
+ permission_protect_wiki_pages: Ochrana Wiki strániek
+ permission_comment_news: Komentovanie noviniek
+ permission_delete_messages: Mazanie správ
+ permission_select_project_modules: Voľba projektových modulov
+ permission_manage_documents: Správa dokumentov
+ permission_edit_wiki_pages: Úprava Wiki strániek
+ permission_add_issue_watchers: Pridanie pozorovateľov
+ permission_view_gantt: Zobrazenie Ganttovho diagramu
+ permission_move_issues: Presun úloh
+ permission_manage_issue_relations: Správa vzťahov medzi úlohami
+ permission_delete_wiki_pages: Mazanie Wiki strániek
+ permission_manage_boards: Správa diskusií
+ permission_delete_wiki_pages_attachments: Mazanie Wiki príloh
+ permission_view_wiki_edits: Zobrazenie Wiki úprav
+ permission_add_messages: Pridanie správ
+ permission_view_messages: Zobrazenie správ
+ permission_manage_files: Správa súborov
+ permission_edit_issue_notes: Úprava poznámok úlohy
+ permission_manage_news: Správa noviniek
+ permission_view_calendar: Zobrazenie kalendára
+ permission_manage_members: Správa členov
+ permission_edit_messages: Úprava správ
+ permission_delete_issues: Mazanie správ
+ permission_view_issue_watchers: Zobrazenie zoznamu pozorovateľov
+ permission_manage_repository: Správa repozitára
+ permission_commit_access: Povoliť prístup
+ permission_browse_repository: Prechádzanie repozitára
+ permission_view_documents: Zobrazenie dokumentov
+ permission_edit_project: Úprava projektu
+ permission_add_issue_notes: Pridanie poznámky úlohy
+ permission_save_queries: Uloženie otáziek
+ permission_view_wiki_pages: Zobrazenie Wiki strániek
+ permission_rename_wiki_pages: Premenovanie Wiki strániek
+ permission_edit_time_entries: Úprava záznamov o strávenom čase
+ permission_edit_own_issue_notes: Úprava vlastných poznámok úlohy
+ setting_gravatar_enabled: Použitie užívateľských Gravatar ikon
+ permission_edit_own_messages: Úprava vlastných správ
+ permission_delete_own_messages: Mazanie vlastných správ
+ text_repository_usernames_mapping: "Vyberte alebo upravte mapovanie medzi užívateľmi systému Redmine a užívateľskými menami nájdenými v logu repozitára.\nUžívatelia s rovnakým prihlasovacím menom alebo emailom v systéme Redmine a repozitára sú mapovaní automaticky."
+ label_example: Príklad
+ label_user_activity: "Aktivita užívateľa {{value}}"
+ label_updated_time_by: "Aktualizované užívateľom {{author}} pred {{age}}"
+ text_diff_truncated: '... Tento rozdielový výpis bol skratený, pretože prekračuje maximálnu veľkosť, ktorá môže byť zobrazená.'
+ setting_diff_max_lines_displayed: Maximálne množstvo zobrazených riadkov rozdielového výpisu
+ text_plugin_assets_writable: Adresár pre pluginy s možnosťou zápisu
+ warning_attachments_not_saved: "{{count}} súbor(y) nemohol(li) byť uložené."
+ field_editable: Editovateľné
+ label_display: Zobrazenie
+ button_create_and_continue: Vytvoriť a pokračovať
+ text_custom_field_possible_values_info: 'Jeden riadok pre každú hodnotu'
+ setting_repository_log_display_limit: Maximálne množstvo revizií zobrazené v logu
+ setting_file_max_size_displayed: Maximálna veľkosť textových súborov zobrazených priamo na stránke
+ field_watcher: Pozorovateľ
+ setting_openid: Povoliť OpenID prihlasovanie a registráciu
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: alebo sa prihlásiť pomocou OpenID
+ field_content: Obsah
+ label_descending: Zostupné
+ label_sort: Zoradenie
+ label_ascending: Rastúce
+ label_date_from_to: Od {{start}} do {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: Táto stránka má {{descendants}} podstránku/y a potomka/ov. Čo chcete vykonať?
+ text_wiki_page_reassign_children: Preradiť podstránky k tejto hlavnej stránke
+ text_wiki_page_nullify_children: Zachovať podstránky ako hlavné stránky
+ text_wiki_page_destroy_children: Vymazať podstránky a všetkých ich potomkov
+ setting_password_min_length: Minimálna dĺžka hesla
+ field_group_by: Skupinové výsledky podľa
+ mail_subject_wiki_content_updated: "'{{page}}' Wiki stránka bola aktualizovaná"
+ label_wiki_content_added: Wiki stránka pridaná
+ mail_subject_wiki_content_added: "'{{page}}' Wiki stránka bola pridaná"
+ mail_body_wiki_content_added: The '{{page}}' Wiki stránka bola pridaná užívateľom {{author}}.
+ permission_add_project: Vytvorenie projektu
+ label_wiki_content_updated: Wiki stránka aktualizovaná
+ mail_body_wiki_content_updated: Wiki stránka '{{page}}' bola aktualizovaná užívateľom {{author}}.
+ setting_repositories_encodings: Kódovanie repozitára
+ setting_new_project_user_role_id: Rola dána non-admin užívateľovi, ktorý vytvorí projekt
+ 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
--- /dev/null
+sl:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "ni vključen na seznamu"
+ exclusion: "je rezerviran"
+ invalid: "je napačen"
+ confirmation: "ne ustreza potrdilu"
+ accepted: "mora biti sprejet"
+ empty: "ne sme biti prazen"
+ blank: "ne sme biti neizpolnjen"
+ too_long: "je predolg"
+ too_short: "je prekratek"
+ wrong_length: "je napačne dolžine"
+ taken: "je že zaseden"
+ not_a_number: "ni število"
+ not_a_date: "ni veljaven datum"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ greater_than_start_date: "mora biti kasnejši kot začeten datum"
+ not_same_project: "ne pripada istemu projektu"
+ circular_dependency: "Ta odnos bi povzročil krožno odvisnost"
+
+ actionview_instancetag_blank_option: Prosimo izberite
+
+ general_text_No: 'Ne'
+ general_text_Yes: 'Da'
+ general_text_no: 'ne'
+ general_text_yes: 'da'
+ general_lang_name: 'Slovenščina'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Račun je bil uspešno posodobljen.
+ notice_account_invalid_creditentials: Napačno uporabniško ime ali geslo
+ notice_account_password_updated: Geslo je bilo uspešno posodobljeno.
+ notice_account_wrong_password: Napačno geslo
+ notice_account_register_done: Račun je bil uspešno ustvarjen. Za aktivacijo potrdite povezavo, ki vam je bila poslana v e-nabiralnik.
+ notice_account_unknown_email: Neznan uporabnik.
+ notice_can_t_change_password: Ta račun za overovljanje uporablja zunanji. Gesla ni mogoče spremeniti.
+ notice_account_lost_email_sent: Poslano vam je bilo e-pismo z navodili za izbiro novega gesla.
+ notice_account_activated: Vaš račun je bil aktiviran. Sedaj se lahko prijavite.
+ notice_successful_create: Ustvarjanje uspelo.
+ notice_successful_update: Posodobitev uspela.
+ notice_successful_delete: Izbris uspel.
+ notice_successful_connection: Povezava uspela.
+ notice_file_not_found: Stran na katero se želite povezati ne obstaja ali pa je bila umaknjena.
+ notice_locking_conflict: Drug uporabnik je posodobil podatke.
+ notice_not_authorized: Nimate privilegijev za dostop do te strani.
+ notice_email_sent: "E-poštno sporočilo je bilo poslano {{value}}"
+ notice_email_error: "Ob pošiljanju e-sporočila je prišlo do napake ({{value}})"
+ notice_feeds_access_key_reseted: Vaš RSS dostopni ključ je bil ponastavljen.
+ notice_failed_to_save_issues: "Neuspelo shranjevanje {{count}} zahtevka na {{total}} izbranem: {{ids}}."
+ notice_no_issue_selected: "Izbran ni noben zahtevek! Prosimo preverite zahtevke, ki jih želite urediti."
+ notice_account_pending: "Vaš račun je bil ustvarjen in čaka na potrditev s strani administratorja."
+ notice_default_data_loaded: Privzete nastavitve so bile uspešno naložene.
+ notice_unable_delete_version: Verzije ni bilo mogoče izbrisati.
+
+ error_can_t_load_default_data: "Privzetih nastavitev ni bilo mogoče naložiti: {{value}}"
+ error_scm_not_found: "Vnos ali revizija v shrambi ni bila najdena ."
+ error_scm_command_failed: "Med vzpostavljem povezave s shrambo je prišlo do napake: {{value}}"
+ error_scm_annotate: "Vnos ne obstaja ali pa ga ni mogoče komentirati."
+ error_issue_not_found_in_project: 'Zahtevek ni bil najden ali pa ne pripada temu projektu'
+
+ mail_subject_lost_password: "Vaše {{value}} geslo"
+ mail_body_lost_password: 'Za spremembo glesla kliknite na naslednjo povezavo:'
+ mail_subject_register: "Aktivacija {{value}} vašega računa"
+ mail_body_register: 'Za aktivacijo vašega računa kliknite na naslednjo povezavo:'
+ mail_body_account_information_external: "Za prijavo lahko uporabite vaš {{value}} račun."
+ mail_body_account_information: Informacije o vašem računu
+ mail_subject_account_activation_request: "{{value}} zahtevek za aktivacijo računa"
+ mail_body_account_activation_request: "Registriral se je nov uporabnik ({{value}}). Račun čaka na vašo odobritev:"
+ mail_subject_reminder: "{{count}} zahtevek(zahtevki) zapadejo v naslednjih dneh"
+ mail_body_reminder: "{{count}} zahtevek(zahtevki), ki so vam dodeljeni bodo zapadli v naslednjih {{days}} dneh:"
+
+ gui_validation_error: 1 napaka
+ gui_validation_error_plural: "{{count}} napak"
+
+ field_name: Ime
+ field_description: Opis
+ field_summary: Povzetek
+ field_is_required: Zahtevano
+ field_firstname: Ime
+ field_lastname: Priimek
+ field_mail: E-naslov
+ field_filename: Datoteka
+ field_filesize: Velikost
+ field_downloads: Prenosi
+ field_author: Avtor
+ field_created_on: Ustvarjen
+ field_updated_on: Posodobljeno
+ field_field_format: Format
+ field_is_for_all: Za vse projekte
+ field_possible_values: Možne vrednosti
+ field_regexp: Regularni izraz
+ field_min_length: Minimalna dolžina
+ field_max_length: Maksimalna dolžina
+ field_value: Vrednost
+ field_category: Kategorija
+ field_title: Naslov
+ field_project: Projekt
+ field_issue: Zahtevek
+ field_status: Status
+ field_notes: Zabeležka
+ field_is_closed: Zahtevek zaprt
+ field_is_default: Privzeta vrednost
+ field_tracker: Vrsta zahtevka
+ field_subject: Tema
+ field_due_date: Do datuma
+ field_assigned_to: Dodeljen
+ field_priority: Prioriteta
+ field_fixed_version: Ciljna verzija
+ field_user: Uporabnik
+ field_role: Vloga
+ field_homepage: Domača stran
+ field_is_public: Javno
+ field_parent: Podprojekt projekta
+ field_is_in_chlog: Zahtevki prikazani v zapisu sprememb
+ field_is_in_roadmap: Zahtevki prikazani na zemljevidu
+ field_login: Prijava
+ field_mail_notification: E-poštna oznanila
+ field_admin: Administrator
+ field_last_login_on: Zadnjič povezan(a)
+ field_language: Jezik
+ field_effective_date: Datum
+ field_password: Geslo
+ field_new_password: Novo geslo
+ field_password_confirmation: Potrditev
+ field_version: Verzija
+ field_type: Tip
+ field_host: Gostitelj
+ field_port: Vrata
+ field_account: Račun
+ field_base_dn: Bazni DN
+ field_attr_login: Oznaka za prijavo
+ field_attr_firstname: Oznaka za ime
+ field_attr_lastname: Oznaka za priimek
+ field_attr_mail: Oznaka za e-naslov
+ field_onthefly: Sprotna izdelava uporabnikov
+ field_start_date: Začetek
+ field_done_ratio: % Narejeno
+ field_auth_source: Način overovljanja
+ field_hide_mail: Skrij moj e-naslov
+ field_comments: Komentar
+ field_url: URL
+ field_start_page: Začetna stran
+ field_subproject: Podprojekt
+ field_hours: Ur
+ field_activity: Aktivnost
+ field_spent_on: Datum
+ field_identifier: Identifikator
+ field_is_filter: Uporabljen kot filter
+ field_issue_to: Povezan zahtevek
+ field_delay: Zamik
+ field_assignable: Zahtevki so lahko dodeljeni tej vlogi
+ field_redirect_existing_links: Preusmeri obstoječe povezave
+ field_estimated_hours: Ocenjen čas
+ field_column_names: Stolpci
+ field_time_zone: Časovni pas
+ field_searchable: Zmožen iskanja
+ field_default_value: Privzeta vrednost
+ field_comments_sorting: Prikaži komentarje
+ field_parent_title: Matična stran
+
+ setting_app_title: Naslov aplikacije
+ setting_app_subtitle: Podnaslov aplikacije
+ setting_welcome_text: Pozdravno besedilo
+ setting_default_language: Privzeti jezik
+ setting_login_required: Zahtevano overovljanje
+ setting_self_registration: Samostojna registracija
+ setting_attachment_max_size: Maksimalna velikost priponk
+ setting_issues_export_limit: Skrajna meja za izvoz zahtevkov
+ setting_mail_from: E-naslov za emisijo
+ setting_bcc_recipients: Prejemniki slepih kopij (bcc)
+ setting_plain_text_mail: navadno e-sporočilo (ne HTML)
+ setting_host_name: Ime gostitelja in pot
+ setting_text_formatting: Oblikovanje besedila
+ setting_wiki_compression: Stiskanje Wiki zgodovine
+ setting_feeds_limit: Meja obsega RSS virov
+ setting_default_projects_public: Novi projekti so privzeto javni
+ setting_autofetch_changesets: Samodejni izvleček zapisa sprememb
+ setting_sys_api_enabled: Omogoči WS za upravljanje shrambe
+ setting_commit_ref_keywords: Sklicne ključne besede
+ setting_commit_fix_keywords: Urejanje ključne besede
+ setting_autologin: Avtomatska prijava
+ setting_date_format: Oblika datuma
+ setting_time_format: Oblika časa
+ setting_cross_project_issue_relations: Dovoli povezave zahtevkov med različnimi projekti
+ setting_issue_list_default_columns: Privzeti stolpci prikazani na seznamu zahtevkov
+ setting_repositories_encodings: Kodiranje shrambe
+ setting_commit_logs_encoding: Kodiranje sporočil ob predaji
+ setting_emails_footer: Noga e-sporočil
+ setting_protocol: Protokol
+ setting_per_page_options: Število elementov na stran
+ setting_user_format: Oblika prikaza uporabnikov
+ setting_activity_days_default: Prikaz dni na aktivnost projekta
+ setting_display_subprojects_issues: Privzeti prikaz zahtevkov podprojektov v glavnem projektu
+ setting_enabled_scm: Omogočen SCM
+ setting_mail_handler_api_enabled: Omogoči WS za prihajajočo e-pošto
+ setting_mail_handler_api_key: API ključ
+ setting_sequential_project_identifiers: Generiraj projektne identifikatorje sekvenčno
+ setting_gravatar_enabled: Uporabljaj Gravatar ikone
+ setting_diff_max_lines_displayed: Maksimalno število prikazanih vrstic različnosti
+
+ permission_edit_project: Uredi projekt
+ permission_select_project_modules: Izberi module projekta
+ permission_manage_members: Uredi člane
+ permission_manage_versions: Uredi verzije
+ permission_manage_categories: Urejanje kategorij zahtevkov
+ permission_add_issues: Dodaj zahtevke
+ permission_edit_issues: Uredi zahtevke
+ permission_manage_issue_relations: Uredi odnose med zahtevki
+ permission_add_issue_notes: Dodaj zabeležke
+ permission_edit_issue_notes: Uredi zabeležke
+ permission_edit_own_issue_notes: Uredi lastne zabeležke
+ permission_move_issues: Premakni zahtevke
+ permission_delete_issues: Izbriši zahtevke
+ permission_manage_public_queries: Uredi javna povpraševanja
+ permission_save_queries: Shrani povpraševanje
+ permission_view_gantt: Poglej gantogram
+ permission_view_calendar: Poglej koledar
+ permission_view_issue_watchers: Oglej si listo spremeljevalcev
+ permission_add_issue_watchers: Dodaj spremljevalce
+ permission_log_time: Beleži porabljen čas
+ permission_view_time_entries: Poglej porabljen čas
+ permission_edit_time_entries: Uredi beležko časa
+ permission_edit_own_time_entries: Uredi beležko lastnega časa
+ permission_manage_news: Uredi novice
+ permission_comment_news: Komentiraj novice
+ permission_manage_documents: Uredi dokumente
+ permission_view_documents: Poglej dokumente
+ permission_manage_files: Uredi datoteke
+ permission_view_files: Poglej datoteke
+ permission_manage_wiki: Uredi wiki
+ permission_rename_wiki_pages: Preimenuj wiki strani
+ permission_delete_wiki_pages: Izbriši wiki strani
+ permission_view_wiki_pages: Poglej wiki
+ permission_view_wiki_edits: Poglej wiki zgodovino
+ permission_edit_wiki_pages: Uredi wiki strani
+ permission_delete_wiki_pages_attachments: Izbriši priponke
+ permission_protect_wiki_pages: Zaščiti wiki strani
+ permission_manage_repository: Uredi shrambo
+ permission_browse_repository: Prebrskaj shrambo
+ permission_view_changesets: Poglej zapis sprememb
+ permission_commit_access: Dostop za predajo
+ permission_manage_boards: Uredi table
+ permission_view_messages: Poglej sporočila
+ permission_add_messages: Objavi sporočila
+ permission_edit_messages: Uredi sporočila
+ permission_edit_own_messages: Uredi lastna sporočila
+ permission_delete_messages: Izbriši sporočila
+ permission_delete_own_messages: Izbriši lastna sporočila
+
+ project_module_issue_tracking: Sledenje zahtevkom
+ project_module_time_tracking: Sledenje časa
+ project_module_news: Novice
+ project_module_documents: Dokumenti
+ project_module_files: Datoteke
+ project_module_wiki: Wiki
+ project_module_repository: Shramba
+ project_module_boards: Table
+
+ label_user: Uporabnik
+ label_user_plural: Uporabniki
+ label_user_new: Nov uporabnik
+ label_project: Projekt
+ label_project_new: Nov projekt
+ label_project_plural: Projekti
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Vsi projekti
+ label_project_latest: Zadnji projekti
+ label_issue: Zahtevek
+ label_issue_new: Nov zahtevek
+ label_issue_plural: Zahtevki
+ label_issue_view_all: Poglej vse zahtevke
+ label_issues_by: "Zahtevki od {{value}}"
+ label_issue_added: Zahtevek dodan
+ label_issue_updated: Zahtevek posodobljen
+ label_document: Dokument
+ label_document_new: Nov dokument
+ label_document_plural: Dokumenti
+ label_document_added: Dokument dodan
+ label_role: Vloga
+ label_role_plural: Vloge
+ label_role_new: Nova vloga
+ label_role_and_permissions: Vloge in dovoljenja
+ label_member: Član
+ label_member_new: Nov član
+ label_member_plural: Člani
+ label_tracker: Vrsta zahtevka
+ label_tracker_plural: Vrste zahtevkov
+ label_tracker_new: Nova vrsta zahtevka
+ label_workflow: Potek dela
+ label_issue_status: Stanje zahtevka
+ label_issue_status_plural: Stanje zahtevkov
+ label_issue_status_new: Novo stanje
+ label_issue_category: Kategorija zahtevka
+ label_issue_category_plural: Kategorije zahtevkov
+ label_issue_category_new: Nova kategorija
+ label_custom_field: Polje po meri
+ label_custom_field_plural: Polja po meri
+ label_custom_field_new: Novo polje po meri
+ label_enumerations: Seznami
+ label_enumeration_new: Nova vrednost
+ label_information: Informacija
+ label_information_plural: Informacije
+ label_please_login: Prosimo prijavite se
+ label_register: Registracija
+ label_password_lost: Izgubljeno geslo
+ label_home: Domov
+ label_my_page: Moja stran
+ label_my_account: Moj račun
+ label_my_projects: Moji projekti
+ label_administration: Upravljanje
+ label_login: Prijavi se
+ label_logout: Odjavi se
+ label_help: Pomoč
+ label_reported_issues: Prijavljeni zahtevki
+ label_assigned_to_me_issues: Zahtevki dodeljeni meni
+ label_last_login: Zadnja povezava
+ label_registered_on: Registriran
+ label_activity: Aktivnost
+ label_overall_activity: Celotna aktivnost
+ label_user_activity: "Aktivnost {{value}}"
+ label_new: Nov
+ label_logged_as: Prijavljen(a) kot
+ label_environment: Okolje
+ label_authentication: Overovitev
+ label_auth_source: Način overovitve
+ label_auth_source_new: Nov način overovitve
+ label_auth_source_plural: Načini overovitve
+ label_subproject_plural: Podprojekti
+ label_and_its_subprojects: "{{value}} in njegovi podprojekti"
+ label_min_max_length: Min - Max dolžina
+ label_list: Seznam
+ label_date: Datum
+ label_integer: Celo število
+ label_float: Decimalno število
+ label_boolean: Boolean
+ label_string: Besedilo
+ label_text: Dolgo besedilo
+ label_attribute: Lastnost
+ label_attribute_plural: Lastnosti
+ label_download: "{{count}} Prenos"
+ label_download_plural: "{{count}} Prenosi"
+ label_no_data: Ni podatkov za prikaz
+ label_change_status: Spremeni stanje
+ label_history: Zgodovina
+ label_attachment: Datoteka
+ label_attachment_new: Nova datoteka
+ label_attachment_delete: Izbriši datoteko
+ label_attachment_plural: Datoteke
+ label_file_added: Datoteka dodana
+ label_report: Poročilo
+ label_report_plural: Poročila
+ label_news: Novica
+ label_news_new: Dodaj novico
+ label_news_plural: Novice
+ label_news_latest: Zadnje novice
+ label_news_view_all: Poglej vse novice
+ label_news_added: Dodane novice
+ label_change_log: Zapisnik spremeb
+ label_settings: Nastavitve
+ label_overview: Pregled
+ label_version: Verzija
+ label_version_new: Nova verzija
+ label_version_plural: Verzije
+ label_confirmation: Potrditev
+ label_export_to: 'Na razpolago tudi v:'
+ label_read: Preberi...
+ label_public_projects: Javni projekti
+ label_open_issues: odpri zahtevek
+ label_open_issues_plural: odpri zahtevke
+ label_closed_issues: zapri zahtevek
+ label_closed_issues_plural: zapri zahtevke
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Skupaj
+ label_permissions: Dovoljenja
+ label_current_status: Trenutno stanje
+ label_new_statuses_allowed: Novi zahtevki dovoljeni
+ label_all: vsi
+ label_none: noben
+ label_nobody: nihče
+ label_next: Naslednji
+ label_previous: Prejšnji
+ label_used_by: V uporabi od
+ label_details: Podrobnosti
+ label_add_note: Dodaj zabeležko
+ label_per_page: Na stran
+ label_calendar: Koledar
+ label_months_from: mesecev od
+ label_gantt: Gantt
+ label_internal: Notranji
+ label_last_changes: "zadnjih {{count}} sprememb"
+ label_change_view_all: Poglej vse spremembe
+ label_personalize_page: Individualiziraj to stran
+ label_comment: Komentar
+ label_comment_plural: Komentarji
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Dodaj komentar
+ label_comment_added: Komentar dodan
+ label_comment_delete: Izbriši komentarje
+ label_query: Iskanje po meri
+ label_query_plural: Iskanja po meri
+ label_query_new: Novo iskanje
+ label_filter_add: Dodaj filter
+ label_filter_plural: Filtri
+ label_equals: je enako
+ label_not_equals: ni enako
+ label_in_less_than: v manj kot
+ label_in_more_than: v več kot
+ label_in: v
+ label_today: danes
+ label_all_time: v vsem času
+ label_yesterday: včeraj
+ label_this_week: ta teden
+ label_last_week: pretekli teden
+ label_last_n_days: "zadnjih {{count}} dni"
+ label_this_month: ta mesec
+ label_last_month: zadnji mesec
+ label_this_year: to leto
+ label_date_range: Razpon datumov
+ label_less_than_ago: manj kot dni nazaj
+ label_more_than_ago: več kot dni nazaj
+ label_ago: dni nazaj
+ label_contains: vsebuje
+ label_not_contains: ne vsebuje
+ label_day_plural: dni
+ label_repository: Shramba
+ label_repository_plural: Shrambe
+ label_browse: Prebrskaj
+ label_modification: "{{count}} sprememba"
+ label_modification_plural: "{{count}} spremembe"
+ label_revision: Revizija
+ label_revision_plural: Revizije
+ label_associated_revisions: Povezane revizije
+ label_added: dodano
+ label_modified: spremenjeno
+ label_copied: kopirano
+ label_renamed: preimenovano
+ label_deleted: izbrisano
+ label_latest_revision: Zadnja revizija
+ label_latest_revision_plural: Zadnje revizije
+ label_view_revisions: Poglej revizije
+ label_max_size: Največja velikost
+ label_sort_highest: Premakni na vrh
+ label_sort_higher: Premakni gor
+ label_sort_lower: Premakni dol
+ label_sort_lowest: Premakni na dno
+ label_roadmap: Načrt
+ label_roadmap_due_in: "Do {{value}}"
+ label_roadmap_overdue: "{{value}} zakasnel"
+ label_roadmap_no_issues: Ni zahtevkov za to verzijo
+ label_search: Išči
+ label_result_plural: Rezultati
+ label_all_words: Vse besede
+ label_wiki: Wiki
+ label_wiki_edit: Wiki urejanje
+ label_wiki_edit_plural: Wiki urejanja
+ label_wiki_page: Wiki stran
+ label_wiki_page_plural: Wiki strani
+ label_index_by_title: Razvrsti po naslovu
+ label_index_by_date: Razvrsti po datumu
+ label_current_version: Trenutna verzija
+ label_preview: Predogled
+ label_feed_plural: RSS viri
+ label_changes_details: Podrobnosti o vseh spremembah
+ label_issue_tracking: Sledenje zahtevkom
+ label_spent_time: Porabljen čas
+ label_f_hour: "{{value}} ura"
+ label_f_hour_plural: "{{value}} ur"
+ label_time_tracking: Sledenje času
+ label_change_plural: Spremembe
+ label_statistics: Statistika
+ label_commits_per_month: Predaj na mesec
+ label_commits_per_author: Predaj na avtorja
+ label_view_diff: Preglej razlike
+ label_diff_inline: znotraj
+ label_diff_side_by_side: vzporedno
+ label_options: Možnosti
+ label_copy_workflow_from: Kopiraj potek dela od
+ label_permissions_report: Poročilo o dovoljenjih
+ label_watched_issues: Spremljani zahtevki
+ label_related_issues: Povezani zahtevki
+ label_applied_status: Uveljavljeno stanje
+ label_loading: Nalaganje...
+ label_relation_new: Nova povezava
+ label_relation_delete: Izbriši povezavo
+ label_relates_to: povezan z
+ label_duplicates: duplikati
+ label_duplicated_by: dupliciral
+ label_blocks: blok
+ label_blocked_by: blokiral
+ label_precedes: ima prednost pred
+ label_follows: sledi
+ label_end_to_start: konec na začetek
+ label_end_to_end: konec na konec
+ label_start_to_start: začetek na začetek
+ label_start_to_end: začetek na konec
+ label_stay_logged_in: Ostani prijavljen(a)
+ label_disabled: onemogoči
+ label_show_completed_versions: Prikaži zaključene verzije
+ label_me: jaz
+ label_board: Forum
+ label_board_new: Nov forum
+ label_board_plural: Forumi
+ label_topic_plural: Teme
+ label_message_plural: Sporočila
+ label_message_last: Zadnje sporočilo
+ label_message_new: Novo sporočilo
+ label_message_posted: Sporočilo dodano
+ label_reply_plural: Odgovori
+ label_send_information: Pošlji informacijo o računu uporabniku
+ label_year: Leto
+ label_month: Mesec
+ label_week: Teden
+ label_date_from: Do
+ label_date_to: Do
+ label_language_based: Glede na uporabnikov jezik
+ label_sort_by: "Razporedi po {{value}}"
+ label_send_test_email: Pošlji testno e-pismo
+ label_feeds_access_key_created_on: "RSS dostopni ključ narejen {{value}} nazaj"
+ label_module_plural: Moduli
+ label_added_time_by: "Dodan {{author}} {{age}} nazaj"
+ label_updated_time_by: "Posodobljen od {{author}} {{age}} nazaj"
+ label_updated_time: "Posodobljen {{value}} nazaj"
+ label_jump_to_a_project: Skoči na projekt...
+ label_file_plural: Datoteke
+ label_changeset_plural: Zapisi sprememb
+ label_default_columns: Privzeti stolpci
+ label_no_change_option: (Ni spremembe)
+ label_bulk_edit_selected_issues: Uredi izbrane zahtevke skupaj
+ label_theme: Tema
+ label_default: Privzeto
+ label_search_titles_only: Preišči samo naslove
+ label_user_mail_option_all: "Za vsak dogodek v vseh mojih projektih"
+ label_user_mail_option_selected: "Za vsak dogodek samo na izbranih projektih..."
+ label_user_mail_option_none: "Samo za zadeve ki jih spremljam ali sem v njih udeležen(a)"
+ label_user_mail_no_self_notified: "Ne želim biti opozorjen(a) na spremembe, ki jih naredim sam(a)"
+ label_registration_activation_by_email: aktivacija računa po e-pošti
+ label_registration_manual_activation: ročna aktivacija računa
+ label_registration_automatic_activation: samodejna aktivacija računa
+ label_display_per_page: "Na stran: {{value}}"
+ label_age: Starost
+ label_change_properties: Sprememba lastnosti
+ label_general: Splošno
+ label_more: Več
+ label_scm: SCM
+ label_plugins: Vtičniki
+ label_ldap_authentication: LDAP overovljanje
+ label_downloads_abbr: D/L
+ label_optional_description: Neobvezen opis
+ label_add_another_file: Dodaj še eno datoteko
+ label_preferences: Preference
+ label_chronological_order: Kronološko
+ label_reverse_chronological_order: Obrnjeno kronološko
+ label_planning: Načrtovanje
+ label_incoming_emails: Prihajajoča e-pošta
+ label_generate_key: Ustvari ključ
+ label_issue_watchers: Spremljevalci
+ label_example: Vzorec
+
+ button_login: Prijavi se
+ button_submit: Pošlji
+ button_save: Shrani
+ button_check_all: Označi vse
+ button_uncheck_all: Odznači vse
+ button_delete: Izbriši
+ button_create: Ustvari
+ button_test: Testiraj
+ button_edit: Uredi
+ button_add: Dodaj
+ button_change: Spremeni
+ button_apply: Uporabi
+ button_clear: Počisti
+ button_lock: Zakleni
+ button_unlock: Odkleni
+ button_download: Prenesi
+ button_list: Seznam
+ button_view: Pogled
+ button_move: Premakni
+ button_back: Nazaj
+ button_cancel: Prekliči
+ button_activate: Aktiviraj
+ button_sort: Razvrsti
+ button_log_time: Beleži čas
+ button_rollback: Povrni na to verzijo
+ button_watch: Spremljaj
+ button_unwatch: Ne spremljaj
+ button_reply: Odgovori
+ button_archive: Arhiviraj
+ button_unarchive: Odarhiviraj
+ button_reset: Ponastavi
+ button_rename: Preimenuj
+ button_change_password: Spremeni geslo
+ button_copy: Kopiraj
+ button_annotate: Zapiši pripombo
+ button_update: Posodobi
+ button_configure: Konfiguriraj
+ button_quote: Citiraj
+
+ status_active: aktivni
+ status_registered: registriran
+ status_locked: zaklenjen
+
+ text_select_mail_notifications: Izberi dejanja za katera naj bodo poslana oznanila preko e-pošto.
+ text_regexp_info: npr. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 pomeni brez omejitev
+ text_project_destroy_confirmation: Ali ste prepričani da želite izbrisati izbrani projekt in vse z njim povezane podatke?
+ text_subprojects_destroy_warning: "Njegov(i) podprojekt(i): {{value}} bodo prav tako izbrisani."
+ text_workflow_edit: Izberite vlogo in zahtevek za urejanje poteka dela
+ text_are_you_sure: Ali ste prepričani?
+ text_tip_task_begin_day: naloga z začetkom na ta dan
+ text_tip_task_end_day: naloga z zaključkom na ta dan
+ text_tip_task_begin_end_day: naloga ki se začne in konča ta dan
+ text_project_identifier_info: 'Dovoljene so samo male črke (a-z), številke in vezaji.<br />Enkrat shranjen identifikator ne more biti spremenjen.'
+ text_caracters_maximum: "največ {{count}} znakov."
+ text_caracters_minimum: "Mora biti vsaj dolg vsaj {{count}} znakov."
+ text_length_between: "Dolžina med {{min}} in {{max}} znakov."
+ text_tracker_no_workflow: Potek dela za to vrsto zahtevka ni določen
+ text_unallowed_characters: Nedovoljeni znaki
+ text_comma_separated: Dovoljenih je več vrednosti (ločenih z vejico).
+ text_issues_ref_in_commit_messages: Zahtevki sklicev in popravkov v sporočilu predaje
+ text_issue_added: "Zahtevek {{id}} je sporočil(a) {{author}}."
+ text_issue_updated: "Zahtevek {{id}} je posodobil(a) {{author}}."
+ text_wiki_destroy_confirmation: Ali ste prepričani da želite izbrisati ta wiki in vso njegovo vsebino?
+ text_issue_category_destroy_question: "Nekateri zahtevki ({{count}}) so dodeljeni tej kategoriji. Kaj želite storiti?"
+ text_issue_category_destroy_assignments: Odstrani naloge v kategoriji
+ text_issue_category_reassign_to: Ponovno dodeli zahtevke tej kategoriji
+ text_user_mail_option: "Na neizbrane projekte boste prejemali le obvestila o zadevah ki jih spremljate ali v katere ste vključeni (npr. zahtevki katerih avtor(ica) ste)"
+ text_no_configuration_data: "Vloge, vrste zahtevkov, statusi zahtevkov in potek dela še niso bili določeni. \nZelo priporočljivo je, da naložite privzeto konfiguracijo, ki jo lahko kasneje tudi prilagodite."
+ text_load_default_configuration: Naloži privzeto konfiguracijo
+ text_status_changed_by_changeset: "Dodano v zapis sprememb {{value}}."
+ text_issues_destroy_confirmation: 'Ali ste prepričani, da želite izbrisati izbrani(e) zahtevek(ke)?'
+ text_select_project_modules: 'Izberite module, ki jih želite omogočiti za ta projekt:'
+ text_default_administrator_account_changed: Spremenjen privzeti administratorski račun
+ text_file_repository_writable: Omogočeno pisanje v shrambo datotek
+ text_rmagick_available: RMagick je na voljo(neobvezno)
+ text_destroy_time_entries_question: "{{hours}} ur je bilo opravljenih na zahtevku, ki ga želite izbrisati. Kaj želite storiti?"
+ text_destroy_time_entries: Izbriši opravljene ure
+ text_assign_time_entries_to_project: Predaj opravljene ure projektu
+ text_reassign_time_entries: 'Prenesi opravljene ure na ta zahtevek:'
+ text_user_wrote: "{{value}} je napisal(a):"
+ text_enumeration_destroy_question: "{{count}} objektov je določenih tej vrednosti."
+ text_enumeration_category_reassign_to: 'Ponastavi jih na to vrednost:'
+ text_email_delivery_not_configured: "E-poštna dostava ni nastavljena in oznanila so onemogočena.\nNastavite vaš SMTP strežnik v config/email.yml in ponovno zaženite aplikacijo da ga omogočite.\n"
+ text_repository_usernames_mapping: "Izberite ali posodobite Redmine uporabnika dodeljenega vsakemu uporabniškemu imenu najdenemu v zapisniku shrambe.\n Uporabniki z enakim Redmine ali shrambinem uporabniškem imenu ali e-poštnem naslovu so samodejno dodeljeni."
+ text_diff_truncated: '... Ta sprememba je bila odsekana ker presega največjo velikost ki je lahko prikazana.'
+
+ default_role_manager: Upravnik
+ default_role_developper: Razvijalec
+ default_role_reporter: Poročevalec
+ default_tracker_bug: Hrošč
+ default_tracker_feature: Funkcija
+ default_tracker_support: Podpora
+ default_issue_status_new: Nov
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Rešen
+ default_issue_status_feedback: Povratna informacija
+ default_issue_status_closed: Zaključen
+ default_issue_status_rejected: Zavrnjen
+ default_doc_category_user: Uporabniška dokumentacija
+ default_doc_category_tech: Tehnična dokumentacija
+ default_priority_low: Nizka
+ default_priority_normal: Običajna
+ default_priority_high: Visoka
+ default_priority_urgent: Urgentna
+ default_priority_immediate: Takojšnje ukrepanje
+ default_activity_design: Oblikovanje
+ default_activity_development: Razvoj
+
+ enumeration_issue_priorities: Prioritete zahtevkov
+ enumeration_doc_categories: Kategorije dokumentov
+ enumeration_activities: Aktivnosti (sledenje časa)
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ field_editable: Editable
+ text_plugin_assets_writable: Plugin assets directory writable
+ label_display: Display
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Serbian default (Cyrillic) translations for Ruby on Rails
+# by Dejan Dimić (dejan.dimic@gmail.com)
+
+"sr":
+ date:
+ formats:
+ default: "%d/%m/%Y"
+ short: "%e %b"
+ long: "%B %e, %Y"
+ only_day: "%e"
+
+ day_names: [Недеља, Понедељак, Уторак, Среда, Четвртак, Петак, Субота]
+ abbr_day_names: [Нед, Пон, Уто, Сре, Чет, Пет, Суб]
+ month_names: [~, Јануар, Фабруар, Март, Април, Мај, Јун, Јул, Август, Септембар, Октобар, Новембар, Децембар]
+ abbr_month_names: [~, Јан, Феб, Мар, Апр, Мај, Јун, Јул, Авг, Сеп, Окт, Нов, Дец]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%a %b %d %H:%M:%S %Z %Y"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ only_second: "%S"
+
+ datetime:
+ formats:
+ default: "%Y-%m-%dT%H:%M:%S%Z"
+
+ am: 'АМ'
+ pm: 'ПМ'
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'пола минуте'
+ less_than_x_seconds:
+ zero: 'мање од 1 секунде'
+ one: 'мање од 1 секунд'
+ few: 'мање од {{count}} секунде'
+ other: 'мање од {{count}} секунди'
+ x_seconds:
+ one: '1 секунда'
+ few: '{{count}} секунде'
+ other: '{{count}} секунди'
+ less_than_x_minutes:
+ zero: 'мање од минута'
+ one: 'мање од 1 минут'
+ other: 'мање од {{count}} минута'
+ x_minutes:
+ one: '1 минут'
+ other: '{{count}} минута'
+ about_x_hours:
+ one: 'око 1 сат'
+ few: 'око {{count}} сата'
+ other: 'око {{count}} сати'
+ x_days:
+ one: '1 дан'
+ other: '{{count}} дана'
+ about_x_months:
+ one: 'око 1 месец'
+ few: 'око {{count}} месеца'
+ other: 'око {{count}} месеци'
+ x_months:
+ one: '1 месец'
+ few: '{{count}} месеца'
+ other: '{{count}} месеци'
+ about_x_years:
+ one: 'око 1 године'
+ other: 'око {{count}} године'
+ over_x_years:
+ one: 'преко 1 године'
+ other: 'преко {{count}} године'
+
+ number:
+ format:
+ precision: 3
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: 'ДИН'
+ precision: 2
+ format: '%n %u'
+ human:
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ sentence_connector: "и"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: 'Нисам успео сачувати {{model}}: 1 грешка.'
+ few: 'Нисам успео сачувати {{model}}: {{count}} грешке.'
+ other: 'Нисам успео сачувати {{model}}: {{count}} грешки.'
+ body: "Молим Вас да проверите следећа поља:"
+ messages:
+ inclusion: "није у листи"
+ exclusion: "није доступно"
+ invalid: "није исправан"
+ confirmation: "се не слаже са својом потврдом"
+ accepted: "мора бити прихваћено"
+ empty: "мора бити дат"
+ blank: "мора бити дат"
+ too_long: "је предугачак (не више од {{count}} карактера)"
+ too_short: "је прекратак (не мање од {{count}} карактера)"
+ wrong_length: "није одговарајуће дужине (мора имати {{count}} карактера)"
+ taken: "је заузето"
+ not_a_number: "није број"
+ greater_than: "мора бити веће од {{count}}"
+ greater_than_or_equal_to: "мора бити веће или једнако {{count}}"
+ equal_to: "кора бити једнако {{count}}"
+ less_than: "мора бити мање од {{count}}"
+ less_than_or_equal_to: "мора бити мање или једнако {{count}}"
+ odd: "мора бити непарно"
+ even: "мора бити парно"
+ greater_than_start_date: "mora biti veći od početnog datuma"
+ not_same_project: "ne pripada istom projektu"
+ circular_dependency: "Ova relacija bi napravila kružnu zavisnost"
+
+ actionview_instancetag_blank_option: Molim izaberite
+
+ general_text_No: 'Ne'
+ general_text_Yes: 'Da'
+ general_text_no: 'ne'
+ general_text_yes: 'da'
+ general_lang_name: 'Srpski'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Nalog je uspešno promenjen.
+ notice_account_invalid_creditentials: Pogrešan korisnik ili lozinka
+ notice_account_password_updated: Lozinka je uspešno promenjena.
+ notice_account_wrong_password: Pogrešna lozinka
+ notice_account_register_done: Nalog je uspešno napravljen. Da bi ste aktivirali vaš nalog kliknite na link koji vam je poslat.
+ notice_account_unknown_email: Nepoznati korisnik.
+ notice_can_t_change_password: Ovaj nalog koristi eksterni izvor prijavljivanja. Ne mogu da promenim šifru.
+ notice_account_lost_email_sent: Email sa uputstvima o izboru nove šifre je poslat na vašu adresu.
+ notice_account_activated: Vaš nalog je aktiviran. Možete se ulogovati.
+ notice_successful_create: Uspešna kreacija.
+ notice_successful_update: Uspešna promena.
+ notice_successful_delete: Uspešno brisanje.
+ notice_successful_connection: Uspešna konekcija.
+ notice_file_not_found: Stranica kojoj pokušavate da pristupite ne postoji ili je uklonjena.
+ notice_locking_conflict: Podaci su promenjeni od strane drugog korisnika.
+ notice_not_authorized: Niste ovlašćeni da pristupite ovoj stranici.
+ notice_email_sent: "Email je poslat {{value}}"
+ notice_email_error: "Došlo je do greške pri slanju maila ({{value}})"
+ notice_feeds_access_key_reseted: Vaš RSS pristup je resetovan.
+ notice_failed_to_save_issues: "Neuspešno snimanje {{count}} kartica na {{total}} izabrano: {{ids}}."
+ notice_no_issue_selected: "Nijedna kartica nije izabrana! Molim, izaberite kartice koje želite za editujete."
+
+ error_scm_not_found: "Unos i/ili revizija ne postoji u spremištu."
+ error_scm_command_failed: "Došlo je do greške pri pristupanju spremištu: {{value}}"
+
+ mail_subject_lost_password: "Vaša {{value}} lozinka"
+ mail_body_lost_password: 'Da biste promenili vašu lozinku, kliknite na sledeći link:'
+ mail_subject_register: "Aktivacija naloga {{value}} "
+ mail_body_register: 'Da biste aktivirali vaš nalog, kliknite na sledeći link:'
+ mail_body_account_information_external: "Mozete koristiti vas nalog {{value}} da bi ste se prikljucili."
+ mail_body_account_information: Informacije o vasem nalogu
+
+ gui_validation_error: 1 greška
+ gui_validation_error_plural: "{{count}} grešaka"
+
+ field_name: Ime
+ field_description: Opis
+ field_summary: Sažetak
+ field_is_required: Zahtevano
+ field_firstname: Ime
+ field_lastname: Prezime
+ field_mail: Email
+ field_filename: Fajl
+ field_filesize: Veličina
+ field_downloads: Preuzimanja
+ field_author: Autor
+ field_created_on: Postavljeno
+ field_updated_on: Promenjeno
+ field_field_format: Format
+ field_is_for_all: Za sve projekte
+ field_possible_values: Moguće vrednosti
+ field_regexp: Regularni izraz
+ field_min_length: Minimalna dužina
+ field_max_length: Maximalna dužina
+ field_value: Vrednost
+ field_category: Kategorija
+ field_title: Naslov
+ field_project: Projekat
+ field_issue: Kartica
+ field_status: Status
+ field_notes: Beleške
+ field_is_closed: Kartica zatvorena
+ field_is_default: Podrazumevana vrednost
+ field_tracker: Vrsta
+ field_subject: Tema
+ field_due_date: Do datuma
+ field_assigned_to: Dodeljeno
+ field_priority: Prioritet
+ field_fixed_version: Verzija
+ field_user: Korisnik
+ field_role: Uloga
+ field_homepage: Homepage
+ field_is_public: Javni
+ field_parent: Potprojekat od
+ field_is_in_chlog: Kartice se prikazuju u dnevniku promena
+ field_is_in_roadmap: Kartice se prikazuju u Redosledu
+ field_login: Korisnik
+ field_mail_notification: Obaveštavanje putem mail-a
+ field_admin: Administrator
+ field_last_login_on: Poslednje prijavljivanje
+ 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: Nalog
+ field_base_dn: Bazni DN
+ field_attr_login: Login atribut
+ field_attr_firstname: Atribut imena
+ field_attr_lastname: Atribut prezimena
+ field_attr_mail: Atribut email-a
+ field_onthefly: Kreacija naloga "On-the-fly"
+ field_start_date: Početak
+ field_done_ratio: % Završeno
+ field_auth_source: Vrsta prijavljivanja
+ field_hide_mail: Sakrij moju email adresu
+ field_comments: Komentar
+ field_url: URL
+ field_start_page: Početna strana
+ field_subproject: Potprojekat
+ field_hours: Sati
+ field_activity: Aktivnost
+ field_spent_on: Datum
+ field_identifier: Identifikator
+ field_is_filter: Korišćen kao filter
+ field_issue_to: Povezano sa karticom
+ field_delay: Odloženo
+ field_assignable: Kartice mogu biti dodeljene ovoj ulozi
+ field_redirect_existing_links: Redirekcija postojećih linkova
+ field_estimated_hours: Procenjeno vreme
+ field_column_names: Kolone
+ field_default_value: Default value
+
+ setting_app_title: Naziv aplikacije
+ setting_app_subtitle: Podnaslov aplikacije
+ setting_welcome_text: Tekst dobrodošlice
+ setting_default_language: Podrazumevani jezik
+ setting_login_required: Prijavljivanje je obavezno
+ setting_self_registration: Samoregistracija je dozvoljena
+ setting_attachment_max_size: Maksimalna velicina Attachment-a
+ setting_issues_export_limit: Max broj kartica u exportu
+ setting_mail_from: Izvorna email adresa
+ setting_host_name: Naziv host-a
+ setting_text_formatting: Formatiranje teksta
+ setting_wiki_compression: Kompresija wiki history-a
+ setting_feeds_limit: Feed content limit
+ setting_autofetch_changesets: Autofetch commits
+ setting_sys_api_enabled: Ukljuci WS za menadžment spremišta
+ setting_commit_ref_keywords: Referentne ključne reči
+ setting_commit_fix_keywords: Fiksne ključne reči
+ setting_autologin: Automatsko prijavljivanje
+ setting_date_format: Format datuma
+ setting_cross_project_issue_relations: Dozvoli relacije kartica između različitih projekata
+ setting_issue_list_default_columns: Podrazumevana kolona se prikazuje na listi kartica
+ setting_repositories_encodings: Kodna stranica spremišta
+ setting_emails_footer: Zaglavlje emaila
+
+ label_example: Primer
+ 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: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Svi projekti
+ label_project_latest: Poslednji projekat
+ label_issue: Kartica
+ label_issue_new: Nova kartica
+ label_issue_plural: Kartice
+ label_issue_view_all: Pregled svih kartica
+ label_document: Dokument
+ label_document_new: Novi dokument
+ label_document_plural: Dokumenti
+ label_role: Uloga
+ label_role_plural: Uloge
+ label_role_new: Nova uloga
+ label_role_and_permissions: Uloge i prava
+ label_member: Član
+ label_member_new: Novi član
+ label_member_plural: Članovi
+ label_tracker: Vrsta
+ label_tracker_plural: Vrste
+ label_tracker_new: Nova vrsta
+ label_workflow: Tok rada
+ label_issue_status: Status kartice
+ label_issue_status_plural: Statusi kartica
+ label_issue_status_new: Novi status
+ label_issue_category: Kategorija kartice
+ label_issue_category_plural: Kategorije kartica
+ label_issue_category_new: Nova kategorija
+ label_custom_field: Korisnički definisano polje
+ label_custom_field_plural: Korisnički definisana polja
+ label_custom_field_new: Novo korisnički definisano polje
+ label_enumerations: Konstante
+ label_enumeration_new: Nova vrednost
+ label_information: Informacija
+ label_information_plural: Informacije
+ label_please_login: Molim prijavite se
+ label_register: Registracija
+ label_password_lost: Izgubljena lozinka
+ label_home: Naslovna stranica
+ label_my_page: Moja stranica
+ label_my_account: Moj nalog
+ label_my_projects: Moji projekti
+ label_administration: Administracija
+ label_login: Korisnik
+ label_logout: Odjavi me
+ label_help: Pomoć
+ label_reported_issues: Prijavljene kartice
+ label_assigned_to_me_issues: Moje kartice
+ label_last_login: Poslednje prijavljivanje
+ label_registered_on: Registrovano
+ label_activity: Aktivnost
+ label_new: Novo
+ label_logged_as: Prijavljen kao
+ label_environment: Environment
+ label_authentication: Prijavljivanje
+ label_auth_source: Način prijavljivanja
+ label_auth_source_new: Novi način prijavljivanja
+ label_auth_source_plural: Načini prijavljivanja
+ label_subproject_plural: Potprojekti
+ label_min_max_length: Min - Max veličina
+ label_list: Liste
+ label_date: Datum
+ label_integer: Integer
+ label_boolean: Boolean
+ label_string: Text
+ label_text: Long text
+ label_attribute: Atribut
+ label_attribute_plural: Atributi
+ label_download: "{{count}} Download"
+ label_download_plural: "{{count}} Downloads"
+ label_no_data: Nema podataka za prikaz
+ label_change_status: Promena statusa
+ label_history: Istorija
+ label_attachment: Fajl
+ label_attachment_new: Novi fajl
+ label_attachment_delete: Brisanje fajla
+ label_attachment_plural: Fajlovi
+ label_report: Izveštaj
+ label_report_plural: Izveštaji
+ label_news: Novosti
+ label_news_new: Dodaj novost
+ label_news_plural: Novosti
+ label_news_latest: Poslednje novosti
+ label_news_view_all: Pregled svih novosti
+ label_change_log: Dnevnik promena
+ label_settings: Podešavanja
+ label_overview: Pregled
+ label_version: Verzija
+ label_version_new: Nova verzija
+ label_version_plural: Verzije
+ label_confirmation: Potvrda
+ label_export_to: Izvoz u
+ label_read: Čitaj...
+ label_public_projects: Javni projekti
+ label_open_issues: Otvoren
+ label_open_issues_plural: Otvoreno
+ label_closed_issues: Zatvoren
+ label_closed_issues_plural: Zatvoreno
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Ukupno
+ label_permissions: Dozvole
+ label_current_status: Trenutni status
+ label_new_statuses_allowed: Novi status je dozvoljen
+ label_all: Sve
+ label_none: nijedan
+ label_nobody: niko
+
+ label_next: Naredni
+ label_previous: Prethodni
+ label_used_by: Korišćen od
+ label_details: Detalji
+ label_add_note: Dodaj belešku
+ label_per_page: Po stranici
+ label_calendar: Kalendar
+ label_months_from: Meseci od
+ label_gantt: Gantt
+ label_internal: Interno
+ label_last_changes: "Poslednjih {{count}} promena"
+ label_change_view_all: Prikaz svih promena
+ label_personalize_page: Personalizuj ovu stranicu
+ label_comment: Komentar
+ label_comment_plural: Komentari
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Dodaj komentar
+ label_comment_added: Komentar dodat
+ label_comment_delete: Brisanje komentara
+ label_query: Korisnički upit
+ label_query_plural: Korisnički upiti
+ label_query_new: Novi upit
+ label_filter_add: Dodaj filter
+ label_filter_plural: Filter
+ label_equals: je
+ label_not_equals: nije
+ label_in_less_than: za manje od
+ label_in_more_than: za više od
+ label_in: za tačno
+ label_today: danas
+ label_this_week: ove nedelje
+ label_less_than_ago: pre manje od
+ label_more_than_ago: pre više od
+ label_ago: pre tačno
+ label_contains: Sadrži
+ label_not_contains: ne sadrži
+ label_day_plural: dana
+ label_repository: Spremište
+ label_browse: Pregled
+ label_modification: "{{count}} promena"
+ label_modification_plural: "{{count}} promena"
+ label_revision: Revizija
+ label_revision_plural: Revizije
+ label_added: dodato
+ label_modified: modifikovano
+ label_deleted: promenjeno
+ label_latest_revision: Poslednja revizija
+ label_latest_revision_plural: Poslednje revizije
+ label_view_revisions: Pregled revizija
+ label_max_size: Maksimalna veličina
+ label_sort_highest: Premesti na vrh
+ label_sort_higher: premesti na gore
+ label_sort_lower: Premesti na dole
+ label_sort_lowest: Premesti na dno
+ label_roadmap: Redosled
+ label_roadmap_due_in: "Završava se za {{value}}"
+ label_roadmap_overdue: "{{value}} kasni"
+ label_roadmap_no_issues: Nema kartica za ovu verziju
+ label_search: Traži
+ label_result_plural: Rezultati
+ label_all_words: Sve reči
+ label_wiki: Wiki
+ label_wiki_edit: Wiki promena
+ label_wiki_edit_plural: Wiki promene
+ label_wiki_page: Wiki stranica
+ label_wiki_page_plural: Wiki stranice
+ label_index_by_title: Indeks po naslovima
+ label_index_by_date: Indeks po datumu
+ label_current_version: Trenutna verzija
+ label_preview: Brzi pregled
+ label_feed_plural: Feeds
+ label_changes_details: Detalji svih promena
+ label_issue_tracking: Praćenje kartica
+ label_spent_time: Utrošeno vremena
+ label_f_hour: "{{value}} časa"
+ label_f_hour_plural: "{{value}} časova"
+ label_time_tracking: Praćenje vremena
+ label_change_plural: Promene
+ label_statistics: Statistika
+ label_commits_per_month: Commit-a po mesecu
+ label_commits_per_author: Commit-a po autoru
+ label_view_diff: Pregled razlika
+ label_diff_inline: uvučeno
+ label_diff_side_by_side: paralelno
+ label_options: Opcije
+ label_copy_workflow_from: Kopiraj tok rada od
+ label_permissions_report: Izveštaj o dozvolama
+ label_watched_issues: Praćene kartice
+ label_related_issues: Povezane kartice
+ label_applied_status: Primenjen status
+ label_loading: Učitavam...
+ label_relation_new: Nova relacija
+ label_relation_delete: Brisanje relacije
+ label_relates_to: u relaciji sa
+ label_duplicates: Duplira
+ label_blocks: blokira
+ label_blocked_by: blokiran od strane
+ label_precedes: prethodi
+ label_follows: sledi
+ label_end_to_start: od kraja do početka
+ label_end_to_end: od kraja do kraja
+ label_start_to_start: od početka do pocetka
+ label_start_to_end: od početka do kraja
+ label_stay_logged_in: Ostani ulogovan
+ label_disabled: Isključen
+ label_show_completed_versions: Prikaži završene verzije
+ label_me: ja
+ label_board: Forum
+ label_board_new: Novi forum
+ label_board_plural: Forumi
+ label_topic_plural: Teme
+ label_message_plural: Poruke
+ label_message_last: Poslednja poruka
+ label_message_new: Nova poruka
+ label_reply_plural: Odgovori
+ label_send_information: Pošalji informaciju o nalogu korisniku
+ label_year: Godina
+ label_month: Mesec
+ label_week: Nedelja
+ label_date_from: Od
+ label_date_to: Do
+ label_language_based: Bazirano na jeziku
+ label_sort_by: "Uredi po {{value}}"
+ label_send_test_email: Pošalji probni email
+ label_feeds_access_key_created_on: "RSS ključ za pristup je napravljen pre {{value}} "
+ label_module_plural: Moduli
+ label_added_time_by: "Dodato pre {{author}} {{age}} "
+ label_updated_time: "Promenjeno pre {{value}} "
+ label_jump_to_a_project: Prebaci se na projekat...
+ label_file_plural: Fajlovi
+ label_changeset_plural: Skupovi promena
+ label_default_columns: Podrazumevane kolone
+ label_no_change_option: (Bez promena)
+ label_bulk_edit_selected_issues: Zajednička promena izabranih kartica
+ label_theme: Tema
+ label_default: Podrazumevana
+ label_search_titles_only: Pretraga samo naslova
+ label_user_mail_option_all: "Za bilo koji događaj na svim mojim projektima"
+ label_user_mail_option_selected: "Za bilo koji događaj za samo izabrane projekte..."
+ label_user_mail_option_none: "Samo za stvari koje pratim ili u kojima učestvujem"
+
+ button_login: Prijavi
+ button_submit: Pošalji
+ button_save: Sačuvaj
+ button_check_all: Označi sve
+ button_uncheck_all: Isključi sve
+ button_delete: Obriši
+ button_create: Napravi
+ button_test: Proveri
+ button_edit: Menjanje
+ button_add: Dodaj
+ button_change: Promeni
+ button_apply: Primeni
+ button_clear: Poništi
+ button_lock: Zaključaj
+ button_unlock: Otključaj
+ button_download: Preuzmi
+ button_list: Spisak
+ button_view: Pregled
+ button_move: Premesti
+ button_back: Nazad
+ button_cancel: Odustani
+ button_activate: Aktiviraj
+ button_sort: Uredi
+ button_log_time: Zapiši vreme
+ button_rollback: Izvrši rollback na ovu verziju
+ button_watch: Prati
+ button_unwatch: Prekini praćenje
+ button_reply: Odgovori
+ button_archive: Arhiviraj
+ button_unarchive: Dearhiviraj
+ button_reset: Poništi
+ button_rename: Promeni ime
+ button_change_password: Promeni lozinku
+
+ status_active: aktivan
+ status_registered: registrovan
+ status_locked: zaključan
+
+ text_select_mail_notifications: Izbor akcija za koje će biti poslato obaveštenje mailom.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 znači bez restrikcija
+ text_project_destroy_confirmation: Da li ste sigurni da želite da izbrišete ovaj projekat i sve njegove podatke?
+ text_workflow_edit: Select a role and a tracker to edit the workflow
+ text_are_you_sure: Da li ste sigurni ?
+ text_tip_task_begin_day: Zadaci koji počinju ovog dana
+ text_tip_task_end_day: zadaci koji se završavaju ovog dana
+ text_tip_task_begin_end_day: Zadaci koji počinju i završavaju se ovog dana
+ text_project_identifier_info: 'mala slova (a-z), brojevi i crtice su dozvoljeni.<br />Jednom snimljen identifikator se ne može menjati'
+ text_caracters_maximum: "{{count}} karaktera maksimalno."
+ text_length_between: "Dužina izmedu {{min}} i {{max}} karaktera."
+ text_tracker_no_workflow: Tok rada nije definisan za ovaj tracker
+ text_unallowed_characters: Nedozvoljeni karakteri
+ text_comma_separated: Višestruke vrednosti su dozvoljene (razdvojene zarezom).
+ text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+ text_issue_added: "Kartica {{id}} je prijavljena (by {{author}})."
+ text_issue_updated: "Kartica {{id}} je promenjena (by {{author}})."
+ text_wiki_destroy_confirmation: Da li ste sigurni da želite da izbrišete ovaj wiki i svu njegovu sadržinu ?
+ text_issue_category_destroy_question: "Neke kartice ({{count}}) su dodeljene ovoj kategoriji. Šta želite da uradite ?"
+ text_issue_category_destroy_assignments: Ukloni dodeljivanje kategorija
+ text_issue_category_reassign_to: Ponovo dodeli kartice ovoj kategoriji
+ text_user_mail_option: "Za neizabrane projekte, primaćete obaveštenja samo o stvarima koje pratite ili u kojima učestvujete (npr. kartice koje ste vi napravili ili koje su vama dodeljene)."
+
+ default_role_manager: Menadžer
+ default_role_developper: Developer
+ default_role_reporter: Reporter
+ default_tracker_bug: Greška
+ default_tracker_feature: Nova osobina
+ default_tracker_support: Podrška
+ default_issue_status_new: Novo
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Rešeno
+ default_issue_status_feedback: Povratna informacija
+ default_issue_status_closed: Zatvoreno
+ default_issue_status_rejected: Odbačeno
+ default_doc_category_user: Korisnička dokumentacija
+ default_doc_category_tech: Tehnička dokumentacija
+ default_priority_low: Nizak
+ default_priority_normal: Redovan
+ default_priority_high: Visok
+ default_priority_urgent: Hitan
+ default_priority_immediate: Odmah
+ default_activity_design: Dizajn
+ default_activity_development: Razvoj
+
+ enumeration_issue_priorities: Prioriteti kartica
+ enumeration_doc_categories: Kategorija dokumenata
+ enumeration_activities: Aktivnosti (praćenje vremena))
+ label_float: Float
+ button_copy: Iskopiraj
+ setting_protocol: Protocol
+ label_user_mail_no_self_notified: "Ne želim da budem obaveštavan o promenama koje sam pravim"
+ setting_time_format: Format vremena
+ label_registration_activation_by_email: aktivacija naloga putem email-a
+ mail_subject_account_activation_request: "{{value}} zahtev za aktivacijom naloga"
+ mail_body_account_activation_request: "Novi korisnik ({{value}}) se registrovao. Njegov nalog čeka vaše odobrenje:"
+ label_registration_automatic_activation: automatska aktivacija naloga
+ label_registration_manual_activation: ručna aktivacija naloga
+ notice_account_pending: "Vaš nalog je napravljen i čeka odobrenje administratora."
+ field_time_zone: Vremenska zona
+ text_caracters_minimum: "Mora biti minimum {{count}} karaktera dugačka."
+ setting_bcc_recipients: '"Blind carbon copy" primaoci (bcc)'
+ button_annotate: Annotate
+ label_issues_by: "Kartice od {{value}}"
+ field_searchable: Searchable
+ label_display_per_page: "Po stranici: {{value}}"
+ setting_per_page_options: Objekata po stranici opcija
+ label_age: Starost
+ notice_default_data_loaded: Default configuration successfully loaded.
+ text_load_default_configuration: Load the default configuration
+ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+ error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
+ button_update: Promeni
+ label_change_properties: Promeni svojstva
+ label_general: Opšte
+ label_repository_plural: Spremišta
+ label_associated_revisions: Dodeljene revizije
+ setting_user_format: Users display format
+ text_status_changed_by_changeset: "Applied in changeset {{value}}."
+ label_more: Još
+ text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+ label_scm: SCM
+ text_select_project_modules: 'Select modules to enable for this project:'
+ label_issue_added: Kartica dodata
+ label_issue_updated: Kartica promenjena
+ label_document_added: Dokument dodat
+ label_message_posted: Poruka dodata
+ label_file_added: Fajl dodat
+ label_news_added: Novost dodata
+ project_module_boards: Boards
+ project_module_issue_tracking: Issue tracking
+ project_module_wiki: Wiki
+ project_module_files: Files
+ project_module_documents: Documents
+ project_module_repository: Repository
+ project_module_news: News
+ project_module_time_tracking: Time tracking
+ text_file_repository_writable: File repository writable
+ text_default_administrator_account_changed: Default administrator account changed
+ text_rmagick_available: RMagick available (optional)
+ button_configure: Configure
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP authentication
+ label_downloads_abbr: D/L
+ label_this_month: ovog meseca
+ label_last_n_days: "poslednjih {{count}} dana"
+ label_all_time: sva vremena
+ label_this_year: ove godine
+ label_date_range: Raspon datuma
+ label_last_week: prošle nedelje
+ label_yesterday: juče
+ label_last_month: prošlog meseca
+ label_add_another_file: Dodaj još jedan fajl
+ label_optional_description: Opcioni opis
+ text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
+ error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
+ text_assign_time_entries_to_project: Assign reported hours to the project
+ text_destroy_time_entries: Delete reported hours
+ text_reassign_time_entries: 'Reassign reported hours to this issue:'
+ setting_activity_days_default: Days displayed on project activity
+ label_chronological_order: U hronološkom redosledu
+ field_comments_sorting: Display comments
+ label_reverse_chronological_order: U obrnutom hronološkom redosledu
+ label_preferences: Preferences
+ setting_display_subprojects_issues: Display subprojects issues on main projects by default
+ label_overall_activity: Ukupna aktivnost
+ setting_default_projects_public: New projects are public by default
+ error_scm_annotate: "The entry does not exist or can not be annotated."
+ label_planning: Planiranje
+ text_subprojects_destroy_warning: "I potprojekti projekta: {{value}} će takođe biti obrisani."
+ label_and_its_subprojects: "{{value}} i potprojekti"
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ text_user_wrote: "{{value}} wrote:"
+ label_duplicated_by: ponovljen kao
+ setting_enabled_scm: Enabled SCM
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ label_incoming_emails: Dolazeće e-poruke
+ label_generate_key: Generiši ključ
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: API key
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: Parent page
+ label_issue_watchers: Posmatrači
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: Quote
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: preimenovano
+ label_copied: iskopirano
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Swedish translation, by Johan Lundström (johanlunds@gmail.com), with parts taken
+# from http://github.com/daniel/swe_rails
+
+sv:
+ number:
+ # Used in number_with_delimiter()
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format:
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ","
+ # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: "."
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 2
+
+ # Used in number_to_currency()
+ currency:
+ format:
+ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
+ format: "%n %u"
+ unit: "kr"
+ # These three are to override number.format and are optional
+ # separator: "."
+ # delimiter: ","
+ # precision: 2
+
+ # Used in number_to_percentage()
+ percentage:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_precision()
+ precision:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_human_size()
+ human:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
+ datetime:
+ distance_in_words:
+ half_a_minute: "en halv minut"
+ less_than_x_seconds:
+ one: "mindre än en sekund"
+ other: "mindre än {{count}} sekunder"
+ x_seconds:
+ one: "en sekund"
+ other: "{{count}} sekunder"
+ less_than_x_minutes:
+ one: "mindre än en minut"
+ other: "mindre än {{count}} minuter"
+ x_minutes:
+ one: "en minut"
+ other: "{{count}} minuter"
+ about_x_hours:
+ one: "ungefär en timme"
+ other: "ungefär {{count}} timmar"
+ x_days:
+ one: "en dag"
+ other: "{{count}} dagar"
+ about_x_months:
+ one: "ungefär en månad"
+ other: "ungefär {{count}} månader"
+ x_months:
+ one: "en månad"
+ other: "{{count}} månader"
+ about_x_years:
+ one: "ungefär ett år"
+ other: "ungefär {{count}} år"
+ over_x_years:
+ one: "mer än ett år"
+ other: "mer än {{count}} år"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "Ett fel förhindrade denna {{model}} från att sparas"
+ other: "{{count}} fel förhindrade denna {{model}} från att sparas"
+ # The variable :count is also available
+ body: "Det var problem med följande fält:"
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "finns inte i listan"
+ exclusion: "är reserverat"
+ invalid: "är ogiltigt"
+ confirmation: "stämmer inte överens"
+ accepted : "måste vara accepterad"
+ empty: "får ej vara tom"
+ blank: "måste anges"
+ too_long: "är för lång (maximum är {{count}} tecken)"
+ too_short: "är för kort (minimum är {{count}} tecken)"
+ wrong_length: "har fel längd (ska vara {{count}} tecken)"
+ taken: "har redan tagits"
+ not_a_number: "är inte ett nummer"
+ greater_than: "måste vara större än {{count}}"
+ greater_than_or_equal_to: "måste vara större än eller lika med {{count}}"
+ equal_to: "måste vara samma som"
+ less_than: "måste vara mindre än {{count}}"
+ less_than_or_equal_to: "måste vara mindre än eller lika med {{count}}"
+ odd: "måste vara udda"
+ even: "måste vara jämnt"
+ greater_than_start_date: "måste vara senare än startdatumet"
+ not_same_project: "tillhör inte samma projekt"
+ circular_dependency: "Denna relation skulle skapa ett cirkulärt beroende"
+
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%e %b"
+ long: "%e %B, %Y"
+
+ day_names: [söndag, måndag, tisdag, onsdag, torsdag, fredag, lördag]
+ abbr_day_names: [sön, mån, tis, ons, tor, fre, lör]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, januari, februari, mars, april, maj, juni, juli, augusti, september, oktober, november, december]
+ abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, aug, sep, okt, nov, dec]
+ # Used in date_select and datime_select.
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%Y-%m-%d %H:%M"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%d %B, %Y %H:%M"
+ am: ""
+ pm: ""
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "och"
+ skip_last_comma: true
+
+ actionview_instancetag_blank_option: Var god välj
+
+ general_text_No: 'Nej'
+ general_text_Yes: 'Ja'
+ general_text_no: 'nej'
+ general_text_yes: 'ja'
+ general_lang_name: 'Svenska'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '1'
+
+ notice_account_updated: Kontot har uppdaterats
+ notice_account_invalid_creditentials: Fel användarnamn eller lösenord
+ notice_account_password_updated: Lösenordet har uppdaterats
+ notice_account_wrong_password: Fel lösenord
+ notice_account_register_done: Kontot har skapats. För att aktivera kontot, klicka på länken i mailet som skickades till dig.
+ notice_account_unknown_email: Okänd användare.
+ notice_can_t_change_password: Detta konto använder en extern autentiseringskälla. Det går inte att byta lösenord.
+ notice_account_lost_email_sent: Ett mail med instruktioner om hur man väljer ett nytt lösenord har skickats till dig.
+ notice_account_activated: Ditt konto har blivit aktiverat. Du kan nu logga in.
+ notice_successful_create: Skapades korrekt.
+ notice_successful_update: Uppdatering lyckades.
+ notice_successful_delete: Borttagning lyckades.
+ notice_successful_connection: Uppkoppling lyckades.
+ notice_file_not_found: Sidan du försökte komma åt existerar inte eller är borttagen.
+ notice_locking_conflict: Data har uppdaterats av en annan användare.
+ notice_not_authorized: Du saknar behörighet att komma åt den här sidan.
+ notice_email_sent: "Ett mail skickades till {{value}}"
+ notice_email_error: "Ett fel inträffade när mail skickades ({{value}})"
+ notice_feeds_access_key_reseted: Din RSS-nyckel återställdes.
+ notice_failed_to_save_issues: "Misslyckades att spara {{count}} ärende(n) på {{total}} valt: {{ids}}."
+ notice_no_issue_selected: "Inget ärende är markerat! Var vänlig, markera de ärenden du vill ändra."
+ notice_account_pending: "Ditt konto skapades och avvaktar nu administratörens godkännande."
+ notice_default_data_loaded: Standardkonfiguration inläst.
+ notice_unable_delete_version: Denna version var inte möjlig att ta bort.
+
+ error_can_t_load_default_data: "Standardkonfiguration gick inte att läsa in: {{value}}"
+ error_scm_not_found: "Inlägg och/eller revision finns inte i detta versionsarkiv."
+ error_scm_command_failed: "Ett fel inträffade vid försök att nå versionsarkivet: {{value}}"
+ error_scm_annotate: "Inlägget existerar inte eller kan inte kommenteras."
+ error_issue_not_found_in_project: 'Ärendet hittades inte eller så tillhör det inte detta projekt'
+ error_no_tracker_in_project: Ingen ärendetyp är associerad med projektet. Vänligen kontrollera projektinställningarna.
+ error_no_default_issue_status: Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (Gå till "Administration -> Ärendestatus").
+
+ warning_attachments_not_saved: "{{count}} fil(er) kunde inte sparas."
+
+ mail_subject_lost_password: "Ditt {{value}} lösenord"
+ mail_body_lost_password: 'För att ändra ditt lösenord, klicka på följande länk:'
+ mail_subject_register: "Din {{value}} kontoaktivering"
+ mail_body_register: 'För att aktivera ditt konto, klicka på följande länk:'
+ mail_body_account_information_external: "Du kan använda ditt {{value}}-konto för att logga in."
+ mail_body_account_information: Din kontoinformation
+ mail_subject_account_activation_request: "{{value}} begäran om kontoaktivering"
+ mail_body_account_activation_request: "En ny användare ({{value}}) har registrerat sig och avvaktar ditt godkännande:"
+ mail_subject_reminder: "{{count}} ärende(n) har deadline under de kommande dagarna"
+ mail_body_reminder: "{{count}} ärende(n) som är tilldelat dig har deadline under de {{days}} dagarna:"
+ mail_subject_wiki_content_added: "'{{page}}' wikisida has lagts till"
+ mail_body_wiki_content_added: The '{{page}}' wikisida has lagts till av {{author}}.
+ mail_subject_wiki_content_updated: "'{{page}}' wikisida har uppdaterats"
+ mail_body_wiki_content_updated: The '{{page}}' wikisida har uppdaterats av {{author}}.
+
+ gui_validation_error: 1 fel
+ gui_validation_error_plural: "{{count}} fel"
+
+ field_name: Namn
+ field_description: Beskrivning
+ field_summary: Sammanfattning
+ field_is_required: Obligatorisk
+ field_firstname: Förnamn
+ field_lastname: Efternamn
+ field_mail: Mail
+ field_filename: Fil
+ field_filesize: Storlek
+ field_downloads: Nerladdningar
+ field_author: Författare
+ field_created_on: Skapad
+ field_updated_on: Uppdaterad
+ field_field_format: Format
+ field_is_for_all: För alla projekt
+ field_possible_values: Möjliga värden
+ field_regexp: Reguljärt uttryck
+ field_min_length: Minimilängd
+ field_max_length: Maxlängd
+ field_value: Värde
+ field_category: Kategori
+ field_title: Titel
+ field_project: Projekt
+ field_issue: Ärende
+ field_status: Status
+ field_notes: Anteckningar
+ field_is_closed: Ärendet är stängt
+ field_is_default: Standardvärde
+ field_tracker: Ärendetyp
+ field_subject: Ämne
+ field_due_date: Deadline
+ field_assigned_to: Tilldelad till
+ field_priority: Prioritet
+ field_fixed_version: Versionsmål
+ field_user: Användare
+ field_role: Roll
+ field_homepage: Hemsida
+ field_is_public: Publik
+ field_parent: Underprojekt till
+ field_is_in_chlog: Visa ärenden i ändringslogg
+ field_is_in_roadmap: Visa ärenden i roadmap
+ field_login: Användarnamn
+ field_mail_notification: Mailnotifieringar
+ field_admin: Administratör
+ field_last_login_on: Senaste inloggning
+ field_language: Språk
+ field_effective_date: Datum
+ field_password: Lösenord
+ field_new_password: Nytt lösenord
+ field_password_confirmation: Bekräfta lösenord
+ field_version: Version
+ field_type: Typ
+ field_host: Värddator
+ field_port: Port
+ field_account: Konto
+ field_base_dn: Bas-DN
+ field_attr_login: Inloggningsattribut
+ field_attr_firstname: Förnamnsattribut
+ field_attr_lastname: Efternamnsattribut
+ field_attr_mail: Mailattribut
+ field_onthefly: Skapa användare on-the-fly
+ field_start_date: Start
+ field_done_ratio: % Klart
+ field_auth_source: Autentiseringsläge
+ field_hide_mail: Dölj min mailadress
+ field_comments: Kommentar
+ field_url: URL
+ field_start_page: Startsida
+ field_subproject: Underprojekt
+ field_hours: Timmar
+ field_activity: Aktivitet
+ field_spent_on: Datum
+ field_identifier: Identifierare
+ field_is_filter: Använd som filter
+ field_issue_to: Relaterade ärenden
+ field_delay: Fördröjning
+ field_assignable: Ärenden kan tilldelas denna roll
+ field_redirect_existing_links: Omdirigera existerande länkar
+ field_estimated_hours: Estimerad tid
+ field_column_names: Kolumner
+ field_time_zone: Tidszon
+ field_searchable: Sökbar
+ field_default_value: Standardvärde
+ field_comments_sorting: Visa kommentarer
+ field_parent_title: Föräldersida
+ field_editable: Redigerbar
+ field_watcher: Bevakare
+ field_identity_url: OpenID URL
+ field_content: Innehåll
+ field_group_by: Gruppera resultat efter
+
+ setting_app_title: Applikationsrubrik
+ setting_app_subtitle: Applikationsunderrubrik
+ setting_welcome_text: Välkomsttext
+ setting_default_language: Standardspråk
+ setting_login_required: Kräver inloggning
+ setting_self_registration: Självregistrering
+ setting_attachment_max_size: Maxstorlek på bilaga
+ setting_issues_export_limit: Exportgräns för ärenden
+ setting_mail_from: Avsändare
+ setting_bcc_recipients: Hemlig kopia (bcc) till mottagare
+ setting_plain_text_mail: Oformaterad text i mail (ingen HTML)
+ setting_host_name: Värddatornamn
+ setting_text_formatting: Textformatering
+ setting_wiki_compression: Komprimering av wikihistorik
+ setting_feeds_limit: Innehållsgräns för Feed
+ setting_default_projects_public: Nya projekt är publika som standard
+ setting_autofetch_changesets: Automatisk hämtning av commits
+ setting_sys_api_enabled: Aktivera WS för versionsarkivhantering
+ setting_commit_ref_keywords: Referens-nyckelord
+ setting_commit_fix_keywords: Fix-nyckelord
+ setting_autologin: Automatisk inloggning
+ setting_date_format: Datumformat
+ setting_time_format: Tidsformat
+ setting_cross_project_issue_relations: Tillåt ärenderelationer mellan projekt
+ setting_issue_list_default_columns: Standardkolumner i ärendelistan
+ setting_repositories_encodings: Teckenuppsättningar för versionsarkiv
+ setting_commit_logs_encoding: Teckenuppsättning för commit-meddelanden
+ setting_emails_footer: Signatur
+ setting_protocol: Protokoll
+ setting_per_page_options: Alternativ, objekt per sida
+ setting_user_format: Visningsformat för användare
+ setting_activity_days_default: Dagar som visas på projektaktivitet
+ setting_display_subprojects_issues: Visa ärenden från underprojekt i huvudprojekt som standard
+ setting_enabled_scm: Aktivera SCM
+ setting_mail_handler_api_enabled: Aktivera WS för inkommande mail
+ setting_mail_handler_api_key: API-nyckel
+ setting_sequential_project_identifiers: Generera projektidentifierare sekventiellt
+ setting_gravatar_enabled: Använd Gravatar-avatarer
+ setting_diff_max_lines_displayed: Maximalt antal synliga rader i diff
+ setting_file_max_size_displayed: Maxstorlek på textfiler som visas inline
+ setting_repository_log_display_limit: Maximalt antal revisioner i filloggen
+ setting_openid: Tillåt inloggning och registrering med OpenID
+ setting_password_min_length: Minsta tillåtna lösenordslängd
+ setting_new_project_user_role_id: Tilldelad roll för en icke-administratör som skapar ett projekt
+
+ permission_add_project: Skapa projekt
+ permission_edit_project: Ändra projekt
+ permission_select_project_modules: Välja projektmoduler
+ permission_manage_members: Hantera medlemmar
+ permission_manage_versions: Hantera versioner
+ permission_manage_categories: Hantera ärendekategorier
+ permission_add_issues: Lägga till ärende
+ permission_edit_issues: Ändra ärende
+ permission_manage_issue_relations: Hantera ärenderelationer
+ permission_add_issue_notes: Lägga till ärendenotering
+ permission_edit_issue_notes: Ändra ärendenoteringar
+ permission_edit_own_issue_notes: Ändra egna ärendenoteringar
+ permission_move_issues: Flytta ärenden
+ permission_delete_issues: Ta bort ärenden
+ permission_manage_public_queries: Hantera publika frågor
+ permission_save_queries: Spara frågor
+ permission_view_gantt: Visa Gantt-schema
+ permission_view_calendar: Visa kalender
+ permission_view_issue_watchers: Visa bevakarlista
+ permission_add_issue_watchers: Lägga till bevakare
+ permission_delete_issue_watchers: Ta bort bevakare
+ permission_log_time: Logga spenderad tid
+ permission_view_time_entries: Visa spenderad tid
+ permission_edit_time_entries: Ändra tidloggningar
+ permission_edit_own_time_entries: Ändra egna tidloggningar
+ permission_manage_news: Hantera nyheter
+ permission_comment_news: Kommentera nyheter
+ permission_manage_documents: Hantera dokument
+ permission_view_documents: Visa dokument
+ permission_manage_files: Hantera filer
+ permission_view_files: Visa filer
+ permission_manage_wiki: Hantera wiki
+ permission_rename_wiki_pages: Byta namn på wikisidor
+ permission_delete_wiki_pages: Ta bort wikisidor
+ permission_view_wiki_pages: Visa wiki
+ permission_view_wiki_edits: Visa wikihistorik
+ permission_edit_wiki_pages: Ändra wikisidor
+ permission_delete_wiki_pages_attachments: Ta bort bilagor
+ permission_protect_wiki_pages: Skydda wikisidor
+ permission_manage_repository: Hantera versionsarkiv
+ permission_browse_repository: Bläddra i versionsarkiv
+ permission_view_changesets: Visa changesets
+ permission_commit_access: Commit-åtkomst
+ permission_manage_boards: Hantera forum
+ permission_view_messages: Visa meddelanden
+ permission_add_messages: Lägg till meddelanden
+ permission_edit_messages: Ändra meddelanden
+ permission_edit_own_messages: Ändra egna meddelanden
+ permission_delete_messages: Ta bort meddelanden
+ permission_delete_own_messages: Ta bort egna meddelanden
+ project_module_issue_tracking: Ärendeuppföljning
+ project_module_time_tracking: Tidsuppföljning
+ project_module_news: Nyheter
+ project_module_documents: Dokument
+ project_module_files: Filer
+ project_module_wiki: Wiki
+ project_module_repository: Versionsarkiv
+ project_module_boards: Forum
+
+ label_user: Användare
+ label_user_plural: Användare
+ label_user_new: Ny användare
+ label_project: Projekt
+ label_project_new: Nytt projekt
+ label_project_plural: Projekt
+ label_x_projects:
+ zero: inga projekt
+ one: 1 projekt
+ other: "{{count}} projekt"
+ label_project_all: Alla projekt
+ label_project_latest: Senaste projekt
+ label_issue: Ärende
+ label_issue_new: Nytt ärende
+ label_issue_plural: Ärenden
+ label_issue_view_all: Visa alla ärenden
+ label_issues_by: "Ärenden {{value}}"
+ label_issue_added: Ärende tillagt
+ label_issue_updated: Ärende uppdaterat
+ label_document: Dokument
+ label_document_new: Nytt dokument
+ label_document_plural: Dokument
+ label_document_added: Dokument tillagt
+ label_role: Roll
+ label_role_plural: Roller
+ label_role_new: Ny roll
+ label_role_and_permissions: Roller och behörigheter
+ label_member: Medlem
+ label_member_new: Ny medlem
+ label_member_plural: Medlemmar
+ label_tracker: Ärendetyp
+ label_tracker_plural: Ärendetyper
+ label_tracker_new: Ny ärendetyp
+ label_workflow: Arbetsflöde
+ label_issue_status: Ärendestatus
+ label_issue_status_plural: Ärendestatus
+ label_issue_status_new: Ny status
+ label_issue_category: Ärendekategori
+ label_issue_category_plural: Ärendekategorier
+ label_issue_category_new: Ny kategori
+ label_custom_field: Användardefinerat fält
+ label_custom_field_plural: Användardefinerade fält
+ label_custom_field_new: Nytt användardefinerat fält
+ label_enumerations: Uppräkningar
+ label_enumeration_new: Nytt värde
+ label_information: Information
+ label_information_plural: Information
+ label_please_login: Var god logga in
+ label_register: Registrera
+ label_login_with_open_id_option: eller logga in med OpenID
+ label_password_lost: Glömt lösenord
+ label_home: Hem
+ label_my_page: Min sida
+ label_my_account: Mitt konto
+ label_my_projects: Mina projekt
+ label_administration: Administration
+ label_login: Logga in
+ label_logout: Logga ut
+ label_help: Hjälp
+ label_reported_issues: Rapporterade ärenden
+ label_assigned_to_me_issues: Ärenden tilldelade till mig
+ label_last_login: Senaste inloggning
+ label_registered_on: Registrerad
+ label_activity: Aktivitet
+ label_overall_activity: All aktivitet
+ label_user_activity: "Aktiviteter för {{value}}"
+ label_new: Ny
+ label_logged_as: Inloggad som
+ label_environment: Miljö
+ label_authentication: Autentisering
+ label_auth_source: Autentiseringsläge
+ label_auth_source_new: Nytt autentiseringsläge
+ label_auth_source_plural: Autentiseringslägen
+ label_subproject_plural: Underprojekt
+ label_and_its_subprojects: "{{value}} och dess underprojekt"
+ label_min_max_length: Min./Max.-längd
+ label_list: Lista
+ label_date: Datum
+ label_integer: Heltal
+ label_float: Flyttal
+ label_boolean: Boolean
+ label_string: Text
+ label_text: Lång text
+ label_attribute: Attribut
+ label_attribute_plural: Attribut
+ label_download: "{{count}} Nerladdning"
+ label_download_plural: "{{count}} Nerladdningar"
+ label_no_data: Ingen data att visa
+ label_change_status: Ändra status
+ label_history: Historia
+ label_attachment: Fil
+ label_attachment_new: Ny fil
+ label_attachment_delete: Ta bort fil
+ label_attachment_plural: Filer
+ label_file_added: Fil tillagd
+ label_report: Rapport
+ label_report_plural: Rapporter
+ label_news: Nyhet
+ label_news_new: Lägg till nyhet
+ label_news_plural: Nyheter
+ label_news_latest: Senaste nyheterna
+ label_news_view_all: Visa alla nyheter
+ label_news_added: Nyhet tillagd
+ label_change_log: Ändringslogg
+ label_settings: Inställningar
+ label_overview: Överblick
+ label_version: Version
+ label_version_new: Ny version
+ label_version_plural: Versioner
+ label_confirmation: Bekräftelse
+ label_export_to: Exportera till
+ label_read: Läs...
+ label_public_projects: Publika projekt
+ label_open_issues: öppen
+ label_open_issues_plural: öppna
+ label_closed_issues: stängd
+ label_closed_issues_plural: stängda
+ label_x_open_issues_abbr_on_total:
+ zero: 0 öppna av {{total}}
+ one: 1 öppen av {{total}}
+ other: "{{count}} öppna av {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 öppna
+ one: 1 öppen
+ other: "{{count}} öppna"
+ label_x_closed_issues_abbr:
+ zero: 0 stängda
+ one: 1 stängd
+ other: "{{count}} stängda"
+ label_total: Total
+ label_permissions: Behörigheter
+ label_current_status: Nuvarande status
+ label_new_statuses_allowed: Nya tillåtna statusvärden
+ label_all: alla
+ label_none: ingen
+ label_nobody: ingen
+ label_next: Nästa
+ label_previous: Föregående
+ label_used_by: Använd av
+ label_details: Detaljer
+ label_add_note: Lägg till anteckning
+ label_per_page: Per sida
+ label_calendar: Kalender
+ label_months_from: månader från
+ label_gantt: Gantt
+ label_internal: Intern
+ label_last_changes: "senaste {{count}} ändringar"
+ label_change_view_all: Visa alla ändringar
+ label_personalize_page: Anpassa denna sida
+ label_comment: Kommentar
+ label_comment_plural: Kommentarer
+ label_x_comments:
+ zero: inga kommentarer
+ one: 1 kommentar
+ other: "{{count}} kommentarer"
+ label_comment_add: Lägg till kommentar
+ label_comment_added: Kommentar tillagd
+ label_comment_delete: Ta bort kommentar
+ label_query: Användardefinerad fråga
+ label_query_plural: Användardefinerade frågor
+ label_query_new: Ny fråga
+ label_filter_add: Lägg till filter
+ label_filter_plural: Filter
+ label_equals: är
+ label_not_equals: är inte
+ label_in_less_than: om mindre än
+ label_in_more_than: om mer än
+ label_greater_or_equal: '>='
+ label_less_or_equal: '<='
+ label_in: om
+ label_today: idag
+ label_all_time: närsom
+ label_yesterday: igår
+ label_this_week: denna vecka
+ label_last_week: senaste veckan
+ label_last_n_days: "senaste {{count}} dagarna"
+ label_this_month: denna månad
+ label_last_month: senaste månaden
+ label_this_year: detta året
+ label_date_range: Datumintervall
+ label_less_than_ago: mindre än dagar sedan
+ label_more_than_ago: mer än dagar sedan
+ label_ago: dagar sedan
+ label_contains: innehåller
+ label_not_contains: innehåller inte
+ label_day_plural: dagar
+ label_repository: Versionsarkiv
+ label_repository_plural: Versionsarkiv
+ label_browse: Bläddra
+ label_modification: "{{count}} ändring"
+ label_modification_plural: "{{count}} ändringar"
+ label_branch: Branch
+ label_tag: Tag
+ label_revision: Revision
+ label_revision_plural: Revisioner
+ label_associated_revisions: Associerade revisioner
+ label_added: tillagd
+ label_modified: modifierad
+ label_copied: kopierad
+ label_renamed: omdöpt
+ label_deleted: borttagen
+ label_latest_revision: Senaste revisionen
+ label_latest_revision_plural: Senaste revisionerna
+ label_view_revisions: Visa revisioner
+ label_view_all_revisions: Visa alla revisioner
+ label_max_size: Maxstorlek
+ label_sort_highest: Flytta till toppen
+ label_sort_higher: Flytta upp
+ label_sort_lower: Flytta ner
+ label_sort_lowest: Flytta till botten
+ label_roadmap: Roadmap
+ label_roadmap_due_in: "Färdig om {{value}}"
+ label_roadmap_overdue: "{{value}} sen"
+ label_roadmap_no_issues: Inga ärenden för denna version
+ label_search: Sök
+ label_result_plural: Resultat
+ label_all_words: Alla ord
+ label_wiki: Wiki
+ label_wiki_edit: Wikiändring
+ label_wiki_edit_plural: Wikiändringar
+ label_wiki_page: Wikisida
+ label_wiki_page_plural: Wikisidor
+ label_index_by_title: Innehåll efter titel
+ label_index_by_date: Innehåll efter datum
+ label_current_version: Nuvarande version
+ label_preview: Förhandsgranska
+ label_feed_plural: Feeds
+ label_changes_details: Detaljer om alla ändringar
+ label_issue_tracking: Ärendeuppföljning
+ label_spent_time: Spenderad tid
+ label_f_hour: "{{value}} timme"
+ label_f_hour_plural: "{{value}} timmar"
+ label_time_tracking: Tidsuppföljning
+ label_change_plural: Ändringar
+ label_statistics: Statistik
+ label_commits_per_month: Commits per månad
+ label_commits_per_author: Commits per författare
+ label_view_diff: Visa skillnader
+ label_diff_inline: i texten
+ label_diff_side_by_side: sida vid sida
+ label_options: Inställningar
+ label_copy_workflow_from: Kopiera arbetsflöde från
+ label_permissions_report: Behörighetsrapport
+ label_watched_issues: Bevakade ärenden
+ label_related_issues: Relaterade ärenden
+ label_applied_status: Tilldelad status
+ label_loading: Laddar...
+ label_relation_new: Ny relation
+ label_relation_delete: Ta bort relation
+ label_relates_to: relaterar till
+ label_duplicates: kopierar
+ label_duplicated_by: kopierad av
+ label_blocks: blockerar
+ label_blocked_by: blockerad av
+ label_precedes: kommer före
+ label_follows: följer
+ label_end_to_start: slut till start
+ label_end_to_end: slut till slut
+ label_start_to_start: start till start
+ label_start_to_end: start till slut
+ label_stay_logged_in: Förbli inloggad
+ label_disabled: inaktiverad
+ label_show_completed_versions: Visa färdiga versioner
+ label_me: mig
+ label_board: Forum
+ label_board_new: Nytt forum
+ label_board_plural: Forum
+ label_topic_plural: Ämnen
+ label_message_plural: Meddelanden
+ label_message_last: Senaste meddelande
+ label_message_new: Nytt meddelande
+ label_message_posted: Meddelande tillagt
+ label_reply_plural: Svar
+ label_send_information: Skicka kontoinformation till användaren
+ label_year: År
+ label_month: Månad
+ label_week: Vecka
+ label_date_from: Från
+ label_date_to: Till
+ label_language_based: Språkbaserad
+ label_sort_by: "Sortera på {{value}}"
+ label_send_test_email: Skicka testmail
+ label_feeds_access_key_created_on: "RSS-nyckel skapad för {{value}} sedan"
+ label_module_plural: Moduler
+ label_added_time_by: "Tillagd av {{author}} för {{age}} sedan"
+ label_updated_time_by: "Uppdaterad av {{author}} för {{age}} sedan"
+ label_updated_time: "Uppdaterad för {{value}} sedan"
+ label_jump_to_a_project: Gå till projekt...
+ label_file_plural: Filer
+ label_changeset_plural: Changesets
+ label_default_columns: Standardkolumner
+ label_no_change_option: (Ingen ändring)
+ label_bulk_edit_selected_issues: Gemensam ändring av markerade ärenden
+ label_theme: Tema
+ label_default: Standard
+ label_search_titles_only: Sök endast i titlar
+ label_user_mail_option_all: "För alla händelser i mina projekt"
+ label_user_mail_option_selected: "För alla händelser i markerade projekt..."
+ label_user_mail_option_none: "Endast för saker jag bevakar eller är involverad i"
+ label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag har gjort"
+ label_registration_activation_by_email: kontoaktivering med mail
+ label_registration_manual_activation: manuell kontoaktivering
+ label_registration_automatic_activation: automatisk kontoaktivering
+ label_display_per_page: "Per sida: {{value}}"
+ label_age: Ålder
+ label_change_properties: Ändra inställningar
+ label_general: Allmänt
+ label_more: Mer
+ label_scm: SCM
+ label_plugins: Tillägg
+ label_ldap_authentication: LDAP-autentisering
+ label_downloads_abbr: Nerl.
+ label_optional_description: Valfri beskrivning
+ label_add_another_file: Lägg till ytterligare en fil
+ label_preferences: Användarinställningar
+ label_chronological_order: I kronologisk ordning
+ label_reverse_chronological_order: I omvänd kronologisk ordning
+ label_planning: Planering
+ label_incoming_emails: Inkommande mail
+ label_generate_key: Generera en nyckel
+ label_issue_watchers: Bevakare
+ label_example: Exempel
+ label_display: Visa
+ label_sort: Sortera
+ label_descending: Fallande
+ label_ascending: Stigande
+ label_date_from_to: Från {{start}} till {{end}}
+ label_wiki_content_added: Wikisida tillagd
+ label_wiki_content_updated: Wikisida uppdaterad
+ label_group: Grupp
+ label_group_plural: Grupper
+ label_group_new: Ny grupp
+ label_time_entry_plural: Spenderad tid
+
+ button_login: Logga in
+ button_submit: Spara
+ button_save: Spara
+ button_check_all: Markera alla
+ button_uncheck_all: Avmarkera alla
+ button_delete: Ta bort
+ button_create: Skapa
+ button_create_and_continue: Skapa och fortsätt
+ button_test: Testa
+ button_edit: Ändra
+ button_add: Lägg till
+ button_change: Ändra
+ button_apply: Verkställ
+ button_clear: Återställ
+ button_lock: Lås
+ button_unlock: Lås upp
+ button_download: Ladda ner
+ button_list: Lista
+ button_view: Visa
+ button_move: Flytta
+ button_back: Tillbaka
+ button_cancel: Avbryt
+ button_activate: Aktivera
+ button_sort: Sortera
+ button_log_time: Logga tid
+ button_rollback: Återställ till denna version
+ button_watch: Bevaka
+ button_unwatch: Stoppa bevakning
+ button_reply: Svara
+ button_archive: Arkivera
+ button_unarchive: Ta bort från arkiv
+ button_reset: Återställ
+ button_rename: Byt namn
+ button_change_password: Ändra lösenord
+ button_copy: Kopiera
+ button_annotate: Kommentera
+ button_update: Uppdatera
+ button_configure: Konfigurera
+ button_quote: Citera
+
+ status_active: aktiv
+ status_registered: registrerad
+ status_locked: låst
+
+ field_active: Aktiv
+
+ text_select_mail_notifications: Välj för vilka händelser mail ska skickas.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 betyder ingen gräns
+ text_project_destroy_confirmation: Är du säker på att du vill ta bort detta projekt och all relaterad data?
+ text_subprojects_destroy_warning: "Alla underprojekt: {{value}} kommer också tas bort."
+ text_workflow_edit: Välj en roll och en ärendetyp för att ändra arbetsflöde
+ text_are_you_sure: Är du säker ?
+ text_journal_changed: "{{label}} ändrad från {{old}} till {{new}}"
+ text_journal_set_to: "{{label}} satt till {{value}}"
+ text_journal_deleted: "{{label}} borttagen ({{old}})"
+ text_journal_added: "{{label}} {{value}} tillagd"
+ text_tip_task_begin_day: arbetsuppgift som börjar denna dag
+ text_tip_task_end_day: arbetsuppgift som slutar denna dag
+ text_tip_task_begin_end_day: arbetsuppgift börjar och slutar denna dag
+ text_project_identifier_info: 'Små bokstäver (a-z), siffror och streck tillåtna.<br />När den är sparad kan identifieraren inte ändras.'
+ text_caracters_maximum: "max {{count}} tecken."
+ text_caracters_minimum: "Måste vara minst {{count}} tecken lång."
+ text_length_between: "Längd mellan {{min}} och {{max}} tecken."
+ text_tracker_no_workflow: Inget arbetsflöde definerat för denna ärendetyp
+ text_unallowed_characters: Otillåtna tecken
+ text_comma_separated: Flera värden tillåtna (kommaseparerade).
+ text_issues_ref_in_commit_messages: Referera och fixa ärenden i commit-meddelanden
+ text_issue_added: "Ärende {{id}} har rapporterats (av {{author}})."
+ text_issue_updated: "Ärende {{id}} har uppdaterats (av {{author}})."
+ text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll ?
+ text_issue_category_destroy_question: "Några ärenden ({{count}}) är tilldelade till denna kategori. Vad vill du göra ?"
+ text_issue_category_destroy_assignments: Ta bort kategoritilldelningar
+ text_issue_category_reassign_to: Återtilldela ärenden till denna kategori
+ text_user_mail_option: "För omarkerade projekt kommer du bara bli underrättad om saker du bevakar eller är inblandad i (T.ex. ärenden du skapat eller tilldelats)."
+ text_no_configuration_data: "Roller, ärendetyper, ärendestatus och arbetsflöden har inte konfigurerats ännu.\nDet rekommenderas att läsa in standardkonfigurationen. Du kommer att kunna göra ändringar efter att den blivit inläst."
+ text_load_default_configuration: Läs in standardkonfiguration
+ text_status_changed_by_changeset: "Tilldelad i changeset {{value}}."
+ text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n) ?'
+ text_select_project_modules: 'Välj vilka moduler som ska vara aktiva för projektet:'
+ text_default_administrator_account_changed: Standardadministratörens konto ändrat
+ text_file_repository_writable: Arkivet för bifogade filer är skrivbar
+ text_plugin_assets_writable: Arkivet för plug-ins är skrivbar
+ text_rmagick_available: RMagick tillgängligt (valfritt)
+ text_destroy_time_entries_question: "{{hours}} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra ?"
+ text_destroy_time_entries: Ta bort rapporterade timmar
+ text_assign_time_entries_to_project: Tilldela rapporterade timmar till projektet
+ text_reassign_time_entries: 'Återtilldela rapporterade timmar till detta ärende:'
+ text_user_wrote: "{{value}} skrev:"
+ text_enumeration_destroy_question: "{{count}} objekt är tilldelade till detta värde."
+ text_enumeration_category_reassign_to: 'Återtilldela till detta värde:'
+ text_email_delivery_not_configured: "Mailfunktionen har inte konfigurerats, och notifieringar via mail kan därför inte skickas.\nKonfigurera din SMTP-server i config/email.yml och starta om applikationen för att aktivera dem."
+ text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller emailadress i både Redmine och versionsarkivet mappas automatiskt."
+ text_diff_truncated: '... Denna diff har förminskats eftersom den överskrider den maximala storlek som kan visas.'
+ text_custom_field_possible_values_info: 'Ett värde per rad'
+ text_wiki_page_destroy_question: Denna sida har {{descendants}} underliggande sidor. Vad vill du göra?
+ text_wiki_page_nullify_children: Behåll undersidor som rotsidor
+ text_wiki_page_destroy_children: Ta bort alla underliggande sidor
+ text_wiki_page_reassign_children: Flytta undersidor till denna föräldersida
+
+ default_role_manager: Projektledare
+ default_role_developper: Utvecklare
+ default_role_reporter: Rapportör
+ default_tracker_bug: Bugg
+ default_tracker_feature: Funktionalitet
+ default_tracker_support: Support
+ default_issue_status_new: Ny
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Löst
+ default_issue_status_feedback: Feedback
+ default_issue_status_closed: Stängd
+ default_issue_status_rejected: Avslagen
+ default_doc_category_user: Användardokumentation
+ default_doc_category_tech: Teknisk dokumentation
+ default_priority_low: Låg
+ default_priority_normal: Normal
+ default_priority_high: Hög
+ default_priority_urgent: Brådskande
+ default_priority_immediate: Omedelbar
+ default_activity_design: Design
+ default_activity_development: Utveckling
+
+ enumeration_issue_priorities: Ärendeprioriteter
+ enumeration_doc_categories: Dokumentkategorier
+ enumeration_activities: Aktiviteter (tidsuppföljning)
+ enumeration_system_activity: Systemaktivitet
+ 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
--- /dev/null
+th:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "ไม่อยู่ในรายการ"
+ exclusion: "ถูกสงวนไว้"
+ invalid: "ไม่ถูกต้อง"
+ confirmation: "พิมพ์ไม่เหมือนเดิม"
+ accepted: "ต้องยอมรับ"
+ empty: "ต้องเติม"
+ blank: "ต้องเติม"
+ too_long: "ยาวเกินไป"
+ too_short: "สั้นเกินไป"
+ wrong_length: "ความยาวไม่ถูกต้อง"
+ taken: "ถูกใช้ไปแล้ว"
+ not_a_number: "ไม่ใช่ตัวเลข"
+ not_a_date: "ไม่ใช่วันที่ ที่ถูกต้อง"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ greater_than_start_date: "ต้องมากกว่าวันเริ่ม"
+ not_same_project: "ไม่ได้อยู่ในโครงการเดียวกัน"
+ circular_dependency: "ความสัมพันธ์อ้างอิงเป็นวงกลม"
+
+ actionview_instancetag_blank_option: กรุณาเลือก
+
+ general_text_No: 'ไม่'
+ general_text_Yes: 'ใช่'
+ general_text_no: 'ไม่'
+ general_text_yes: 'ใช่'
+ general_lang_name: 'Thai (ไทย)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: Windows-874
+ general_pdf_encoding: cp874
+ general_first_day_of_week: '1'
+
+ notice_account_updated: บัญชีได้ถูกปรับปรุงแล้ว.
+ notice_account_invalid_creditentials: ชื้ผู้ใช้หรือรหัสผ่านไม่ถูกต้อง
+ notice_account_password_updated: รหัสได้ถูกปรับปรุงแล้ว.
+ notice_account_wrong_password: รหัสผ่านไม่ถูกต้อง
+ notice_account_register_done: บัญชีถูกสร้างแล้ว. กรุณาเช็คเมล์ แล้วคลิ๊กที่ลิงค์ในอีเมล์เพื่อเปิดใช้บัญชี
+ notice_account_unknown_email: ไม่มีผู้ใช้ที่ใช้อีเมล์นี้.
+ notice_can_t_change_password: บัญชีนี้ใช้การยืนยันตัวตนจากแหล่งภายนอก. ไม่สามารถปลี่ยนรหัสผ่านได้.
+ notice_account_lost_email_sent: เราได้ส่งอีเมล์พร้อมวิธีการสร้างรหัีสผ่านใหม่ให้คุณแล้ว กรุณาเช็คเมล์.
+ notice_account_activated: บัญชีของคุณได้เปิดใช้แล้ว. ตอนนี้คุณสามารถเข้าสู่ระบบได้แล้ว.
+ notice_successful_create: สร้างเสร็จแล้ว.
+ notice_successful_update: ปรับปรุงเสร็จแล้ว.
+ notice_successful_delete: ลบเสร็จแล้ว.
+ notice_successful_connection: ติดต่อสำเร็จแล้ว.
+ notice_file_not_found: หน้าที่คุณต้องการดูไม่มีอยู่จริง หรือถูกลบไปแล้ว.
+ notice_locking_conflict: ข้อมูลถูกปรับปรุงโดยผู้ใช้คนอื่น.
+ notice_not_authorized: คุณไม่มีสิทธิเข้าถึงหน้านี้.
+ notice_email_sent: "อีเมล์ได้ถูกส่งถึง {{value}}"
+ notice_email_error: "เกิดความผิดพลาดขณะกำส่งอีเมล์ ({{value}})"
+ notice_feeds_access_key_reseted: RSS access key ของคุณถูก reset แล้ว.
+ notice_failed_to_save_issues: "{{count}} ปัญหาจาก {{total}} ปัญหาที่ถูกเลือกไม่สามารถจัดเก็บ: {{ids}}."
+ notice_no_issue_selected: "ไม่มีปัญหาที่ถูกเลือก! กรุณาเลือกปัญหาที่คุณต้องการแก้ไข."
+ notice_account_pending: "บัญชีของคุณสร้างเสร็จแล้ว ขณะนี้รอการอนุมัติจากผู้บริหารจัดการ."
+ notice_default_data_loaded: ค่าเริ่มต้นโหลดเสร็จแล้ว.
+
+ error_can_t_load_default_data: "ค่าเริ่มต้นโหลดไม่สำเร็จ: {{value}}"
+ error_scm_not_found: "ไม่พบรุ่นที่ต้องการในแหล่งเก็บต้นฉบับ."
+ error_scm_command_failed: "เกิดความผิดพลาดในการเข้าถึงแหล่งเก็บต้นฉบับ: {{value}}"
+ error_scm_annotate: "entry ไม่มีอยู่จริง หรือไม่สามารถเขียนหมายเหตุประกอบ."
+ error_issue_not_found_in_project: 'ไม่พบปัญหานี้ หรือปัญหาไม่ได้อยู่ในโครงการนี้'
+
+ mail_subject_lost_password: "รหัสผ่าน {{value}} ของคุณ"
+ mail_body_lost_password: 'คลิ๊กที่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:'
+ mail_subject_register: "เปิดบัญชี {{value}} ของคุณ"
+ mail_body_register: 'คลิ๊กที่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:'
+ mail_body_account_information_external: "คุณสามารถใช้บัญชี {{value}} เพื่อเข้าสู่ระบบ."
+ mail_body_account_information: ข้อมูลบัญชีของคุณ
+ mail_subject_account_activation_request: "กรุณาเปิดบัญชี {{value}}"
+ mail_body_account_activation_request: "ผู้ใช้ใหม่ ({{value}}) ได้ลงทะเบียน. บัญชีของเขากำลังรออนุมัติ:"
+
+ gui_validation_error: 1 ข้อผิดพลาด
+ gui_validation_error_plural: "{{count}} ข้อผิดพลาด"
+
+ field_name: ชื่อ
+ field_description: รายละเอียด
+ field_summary: สรุปย่อ
+ field_is_required: ต้องใส่
+ field_firstname: ชื่อ
+ field_lastname: นามสกุล
+ field_mail: อีเมล์
+ field_filename: แฟ้ม
+ field_filesize: ขนาด
+ field_downloads: ดาวน์โหลด
+ field_author: ผู้แต่ง
+ field_created_on: สร้าง
+ field_updated_on: ปรับปรุง
+ field_field_format: รูปแบบ
+ field_is_for_all: สำหรับทุกโครงการ
+ field_possible_values: ค่าที่เป็นไปได้
+ field_regexp: Regular expression
+ field_min_length: สั้นสุด
+ field_max_length: ยาวสุด
+ field_value: ค่า
+ field_category: ประเภท
+ field_title: ชื่อเรื่อง
+ field_project: โครงการ
+ field_issue: ปัญหา
+ field_status: สถานะ
+ field_notes: บันทึก
+ field_is_closed: ปัญหาจบ
+ field_is_default: ค่าเริ่มต้น
+ field_tracker: การติดตาม
+ field_subject: เรื่อง
+ field_due_date: วันครบกำหนด
+ field_assigned_to: มอบหมายให้
+ field_priority: ความสำคัญ
+ field_fixed_version: รุ่น
+ field_user: ผู้ใช้
+ field_role: บทบาท
+ field_homepage: หน้าแรก
+ field_is_public: สาธารณะ
+ field_parent: โครงการย่อยของ
+ field_is_in_chlog: ปัญหาแสดงใน รายกาเปลี่ยนแปลง
+ field_is_in_roadmap: ปัญหาแสดงใน แผนงาน
+ field_login: ชื่อที่ใช้เข้าระบบ
+ field_mail_notification: การแจ้งเตือนทางอีเมล์
+ field_admin: ผู้บริหารจัดการ
+ field_last_login_on: เข้าระบบครั้งสุดท้าย
+ field_language: ภาษา
+ field_effective_date: วันที่
+ field_password: รหัสผ่าน
+ field_new_password: รหัสผ่านใหม่
+ field_password_confirmation: ยืนยันรหัสผ่าน
+ field_version: รุ่น
+ field_type: ชนิด
+ field_host: โฮสต์
+ field_port: พอร์ต
+ field_account: บัญชี
+ field_base_dn: Base DN
+ field_attr_login: เข้าระบบ attribute
+ field_attr_firstname: ชื่อ attribute
+ field_attr_lastname: นามสกุล attribute
+ field_attr_mail: อีเมล์ attribute
+ field_onthefly: สร้างผู้ใช้ทันที
+ field_start_date: เริ่ม
+ field_done_ratio: % สำเร็จ
+ field_auth_source: วิธีการยืนยันตัวตน
+ field_hide_mail: ซ่อนอีเมล์ของฉัน
+ field_comments: ความเห็น
+ field_url: URL
+ field_start_page: หน้าเริ่มต้น
+ field_subproject: โครงการย่อย
+ field_hours: ชั่วโมง
+ field_activity: กิจกรรม
+ field_spent_on: วันที่
+ field_identifier: ชื่อเฉพาะ
+ field_is_filter: ใช้เป็นตัวกรอง
+ field_issue_to: ปัญหาที่เกี่ยวข้อง
+ field_delay: เลื่อน
+ field_assignable: ปัญหาสามารถมอบหมายให้คนที่ทำบทบาทนี้
+ field_redirect_existing_links: ย้ายจุดเชื่อมโยงนี้
+ field_estimated_hours: เวลาที่ใช้โดยประมาณ
+ field_column_names: สดมภ์
+ field_time_zone: ย่านเวลา
+ field_searchable: ค้นหาได้
+ field_default_value: ค่าเริ่มต้น
+ field_comments_sorting: แสดงความเห็น
+
+ setting_app_title: ชื่อโปรแกรม
+ setting_app_subtitle: ชื่อโปรแกรมรอง
+ setting_welcome_text: ข้อความต้อนรับ
+ setting_default_language: ภาษาเริ่มต้น
+ setting_login_required: ต้องป้อนผู้ใช้-รหัสผ่าน
+ setting_self_registration: ลงทะเบียนด้วยตนเอง
+ setting_attachment_max_size: ขนาดแฟ้มแนบสูงสุด
+ setting_issues_export_limit: การส่งออกปัญหาสูงสุด
+ setting_mail_from: อีเมล์ที่ใช้ส่ง
+ setting_bcc_recipients: ไม่ระบุชื่อผู้รับ (bcc)
+ setting_host_name: ชื่อโฮสต์
+ setting_text_formatting: การจัดรูปแบบข้อความ
+ setting_wiki_compression: บีบอัดประวัติ Wiki
+ setting_feeds_limit: จำนวน Feed
+ setting_default_projects_public: โครงการใหม่มีค่าเริ่มต้นเป็น สาธารณะ
+ setting_autofetch_changesets: ดึง commits อัตโนมัติ
+ setting_sys_api_enabled: เปิดใช้ WS สำหรับการจัดการที่เก็บต้นฉบับ
+ setting_commit_ref_keywords: คำสำคัญ Referencing
+ setting_commit_fix_keywords: คำสำคัญ Fixing
+ setting_autologin: เข้าระบบอัตโนมัติ
+ setting_date_format: รูปแบบวันที่
+ setting_time_format: รูปแบบเวลา
+ setting_cross_project_issue_relations: อนุญาตให้ระบุปัญหาข้ามโครงการ
+ setting_issue_list_default_columns: สดมภ์เริ่มต้นแสดงในรายการปัญหา
+ setting_repositories_encodings: การเข้ารหัสที่เก็บต้นฉบับ
+ setting_emails_footer: คำลงท้ายอีเมล์
+ setting_protocol: Protocol
+ setting_per_page_options: ตัวเลือกจำนวนต่อหน้า
+ setting_user_format: รูปแบบการแสดงชื่อผู้ใช้
+ setting_activity_days_default: จำนวนวันที่แสดงในกิจกรรมของโครงการ
+ setting_display_subprojects_issues: แสดงปัญหาของโครงการย่อยในโครงการหลัก
+
+ project_module_issue_tracking: การติดตามปัญหา
+ project_module_time_tracking: การใช้เวลา
+ project_module_news: ข่าว
+ project_module_documents: เอกสาร
+ project_module_files: แฟ้ม
+ project_module_wiki: Wiki
+ project_module_repository: ที่เก็บต้นฉบับ
+ project_module_boards: กระดานข้อความ
+
+ label_user: ผู้ใช้
+ label_user_plural: ผู้ใช้
+ label_user_new: ผู้ใช้ใหม่
+ label_project: โครงการ
+ label_project_new: โครงการใหม่
+ label_project_plural: โครงการ
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: โครงการทั้งหมด
+ label_project_latest: โครงการล่าสุด
+ label_issue: ปัญหา
+ label_issue_new: ปัญหาใหม่
+ label_issue_plural: ปัญหา
+ label_issue_view_all: ดูปัญหาทั้งหมด
+ label_issues_by: "ปัญหาโดย {{value}}"
+ label_issue_added: ปัญหาถูกเพิ่ม
+ label_issue_updated: ปัญหาถูกปรับปรุง
+ label_document: เอกสาร
+ label_document_new: เอกสารใหม่
+ label_document_plural: เอกสาร
+ label_document_added: เอกสารถูกเพิ่ม
+ label_role: บทบาท
+ label_role_plural: บทบาท
+ label_role_new: บทบาทใหม่
+ label_role_and_permissions: บทบาทและสิทธิ
+ label_member: สมาชิก
+ label_member_new: สมาชิกใหม่
+ label_member_plural: สมาชิก
+ label_tracker: การติดตาม
+ label_tracker_plural: การติดตาม
+ label_tracker_new: การติดตามใหม่
+ label_workflow: ลำดับงาน
+ label_issue_status: สถานะของปัญหา
+ label_issue_status_plural: สถานะของปัญหา
+ label_issue_status_new: สถานะใหม
+ label_issue_category: ประเภทของปัญหา
+ label_issue_category_plural: ประเภทของปัญหา
+ label_issue_category_new: ประเภทใหม่
+ label_custom_field: เขตข้อมูลแบบระบุเอง
+ label_custom_field_plural: เขตข้อมูลแบบระบุเอง
+ label_custom_field_new: สร้างเขตข้อมูลแบบระบุเอง
+ label_enumerations: รายการ
+ label_enumeration_new: สร้างใหม่
+ label_information: ข้อมูล
+ label_information_plural: ข้อมูล
+ label_please_login: กรุณาเข้าระบบก่อน
+ label_register: ลงทะเบียน
+ label_password_lost: ลืมรหัสผ่าน
+ label_home: หน้าแรก
+ label_my_page: หน้าของฉัน
+ label_my_account: บัญชีของฉัน
+ label_my_projects: โครงการของฉัน
+ label_administration: บริหารจัดการ
+ label_login: เข้าระบบ
+ label_logout: ออกระบบ
+ label_help: ช่วยเหลือ
+ label_reported_issues: ปัญหาที่แจ้งไว้
+ label_assigned_to_me_issues: ปัญหาที่มอบหมายให้ฉัน
+ label_last_login: ติดต่อครั้งสุดท้าย
+ label_registered_on: ลงทะเบียนเมื่อ
+ label_activity: กิจกรรม
+ label_activity_plural: กิจกรรม
+ label_activity_latest: กิจกรรมล่าสุด
+ label_overall_activity: กิจกรรมโดยรวม
+ label_new: ใหม่
+ label_logged_as: เข้าระบบในชื่อ
+ label_environment: สภาพแวดล้อม
+ label_authentication: การยืนยันตัวตน
+ label_auth_source: วิธีการการยืนยันตัวตน
+ label_auth_source_new: สร้างวิธีการยืนยันตัวตนใหม่
+ label_auth_source_plural: วิธีการ Authentication
+ label_subproject_plural: โครงการย่อย
+ label_min_max_length: สั้น-ยาว สุดที่
+ label_list: รายการ
+ label_date: วันที่
+ label_integer: จำนวนเต็ม
+ label_float: จำนวนจริง
+ label_boolean: ถูกผิด
+ label_string: ข้อความ
+ label_text: ข้อความขนาดยาว
+ label_attribute: คุณลักษณะ
+ label_attribute_plural: คุณลักษณะ
+ label_download: "{{count}} ดาวน์โหลด"
+ label_download_plural: "{{count}} ดาวน์โหลด"
+ label_no_data: จำนวนข้อมูลที่แสดง
+ label_change_status: เปลี่ยนสถานะ
+ label_history: ประวัติ
+ label_attachment: แฟ้ม
+ label_attachment_new: แฟ้มใหม่
+ label_attachment_delete: ลบแฟ้ม
+ label_attachment_plural: แฟ้ม
+ label_file_added: แฟ้มถูกเพิ่ม
+ label_report: รายงาน
+ label_report_plural: รายงาน
+ label_news: ข่าว
+ label_news_new: เพิ่มข่าว
+ label_news_plural: ข่าว
+ label_news_latest: ข่าวล่าสุด
+ label_news_view_all: ดูข่าวทั้งหมด
+ label_news_added: ข่าวถูกเพิ่ม
+ label_change_log: บันทึกการเปลี่ยนแปลง
+ label_settings: ปรับแต่ง
+ label_overview: ภาพรวม
+ label_version: รุ่น
+ label_version_new: รุ่นใหม่
+ label_version_plural: รุ่น
+ label_confirmation: ยืนยัน
+ label_export_to: 'รูปแบบอื่นๆ :'
+ label_read: อ่าน...
+ label_public_projects: โครงการสาธารณะ
+ label_open_issues: เปิด
+ label_open_issues_plural: เปิด
+ label_closed_issues: ปิด
+ label_closed_issues_plural: ปิด
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: จำนวนรวม
+ label_permissions: สิทธิ
+ label_current_status: สถานะปัจจุบัน
+ label_new_statuses_allowed: อนุญาตให้มีสถานะใหม่
+ label_all: ทั้งหมด
+ label_none: ไม่มี
+ label_nobody: ไม่มีใคร
+ label_next: ต่อไป
+ label_previous: ก่อนหน้า
+ label_used_by: ถูกใช้โดย
+ label_details: รายละเอียด
+ label_add_note: เพิ่มบันทึก
+ label_per_page: ต่อหน้า
+ label_calendar: ปฏิทิน
+ label_months_from: เดือนจาก
+ label_gantt: Gantt
+ label_internal: ภายใน
+ label_last_changes: "last {{count}} เปลี่ยนแปลง"
+ label_change_view_all: ดูการเปลี่ยนแปลงทั้งหมด
+ label_personalize_page: ปรับแต่งหน้านี้
+ label_comment: ความเห็น
+ label_comment_plural: ความเห็น
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: เพิ่มความเห็น
+ label_comment_added: ความเห็นถูกเพิ่ม
+ label_comment_delete: ลบความเห็น
+ label_query: แบบสอบถามแบบกำหนดเอง
+ label_query_plural: แบบสอบถามแบบกำหนดเอง
+ label_query_new: แบบสอบถามใหม่
+ label_filter_add: เพิ่มตัวกรอง
+ label_filter_plural: ตัวกรอง
+ label_equals: คือ
+ label_not_equals: ไม่ใช่
+ label_in_less_than: น้อยกว่า
+ label_in_more_than: มากกว่า
+ label_in: ในช่วง
+ label_today: วันนี้
+ label_all_time: ตลอดเวลา
+ label_yesterday: เมื่อวาน
+ label_this_week: อาทิตย์นี้
+ label_last_week: อาทิตย์ที่แล้ว
+ label_last_n_days: "{{count}} วันย้อนหลัง"
+ label_this_month: เดือนนี้
+ label_last_month: เดือนที่แล้ว
+ label_this_year: ปีนี้
+ label_date_range: ช่วงวันที่
+ label_less_than_ago: น้อยกว่าหนึ่งวัน
+ label_more_than_ago: มากกว่าหนึ่งวัน
+ label_ago: วันผ่านมาแล้ว
+ label_contains: มี...
+ label_not_contains: ไม่มี...
+ label_day_plural: วัน
+ label_repository: ที่เก็บต้นฉบับ
+ label_repository_plural: ที่เก็บต้นฉบับ
+ label_browse: เปิดหา
+ label_modification: "{{count}} เปลี่ยนแปลง"
+ label_modification_plural: "{{count}} เปลี่ยนแปลง"
+ label_revision: การแก้ไข
+ label_revision_plural: การแก้ไข
+ label_associated_revisions: การแก้ไขที่เกี่ยวข้อง
+ label_added: ถูกเพิ่ม
+ label_modified: ถูกแก้ไข
+ label_deleted: ถูกลบ
+ label_latest_revision: รุ่นการแก้ไขล่าสุด
+ label_latest_revision_plural: รุ่นการแก้ไขล่าสุด
+ label_view_revisions: ดูการแก้ไข
+ label_max_size: ขนาดใหญ่สุด
+ label_sort_highest: ย้ายไปบนสุด
+ label_sort_higher: ย้ายขึ้น
+ label_sort_lower: ย้ายลง
+ label_sort_lowest: ย้ายไปล่างสุด
+ label_roadmap: แผนงาน
+ label_roadmap_due_in: "ถึงกำหนดใน {{value}}"
+ label_roadmap_overdue: "{{value}} ช้ากว่ากำหนด"
+ label_roadmap_no_issues: ไม่มีปัญหาสำหรับรุ่นนี้
+ label_search: ค้นหา
+ label_result_plural: ผลการค้นหา
+ label_all_words: ทุกคำ
+ label_wiki: Wiki
+ label_wiki_edit: แก้ไข Wiki
+ label_wiki_edit_plural: แก้ไข Wiki
+ label_wiki_page: หน้า Wiki
+ label_wiki_page_plural: หน้า Wiki
+ label_index_by_title: เรียงตามชื่อเรื่อง
+ label_index_by_date: เรียงตามวัน
+ label_current_version: รุ่นปัจจุบัน
+ label_preview: ตัวอย่างก่อนจัดเก็บ
+ label_feed_plural: Feeds
+ label_changes_details: รายละเอียดการเปลี่ยนแปลงทั้งหมด
+ label_issue_tracking: ติดตามปัญหา
+ label_spent_time: เวลาที่ใช้
+ label_f_hour: "{{value}} ชั่วโมง"
+ label_f_hour_plural: "{{value}} ชั่วโมง"
+ label_time_tracking: ติดตามการใช้เวลา
+ label_change_plural: เปลี่ยนแปลง
+ label_statistics: สถิติ
+ label_commits_per_month: Commits ต่อเดือน
+ label_commits_per_author: Commits ต่อผู้แต่ง
+ label_view_diff: ดูความแตกต่าง
+ label_diff_inline: inline
+ label_diff_side_by_side: side by side
+ label_options: ตัวเลือก
+ label_copy_workflow_from: คัดลอกลำดับงานจาก
+ label_permissions_report: รายงานสิทธิ
+ label_watched_issues: เฝ้าดูปัญหา
+ label_related_issues: ปัญหาที่เกี่ยวข้อง
+ label_applied_status: จัดเก็บสถานะ
+ label_loading: กำลังโหลด...
+ label_relation_new: ความสัมพันธ์ใหม่
+ label_relation_delete: ลบความสัมพันธ์
+ label_relates_to: สัมพันธ์กับ
+ label_duplicates: ซ้ำ
+ label_blocks: กีดกัน
+ label_blocked_by: กีดกันโดย
+ label_precedes: นำหน้า
+ label_follows: ตามหลัง
+ label_end_to_start: จบ-เริ่ม
+ label_end_to_end: จบ-จบ
+ label_start_to_start: เริ่ม-เริ่ม
+ label_start_to_end: เริ่ม-จบ
+ label_stay_logged_in: อยู่ในระบบต่อ
+ label_disabled: ไม่ใช้งาน
+ label_show_completed_versions: แสดงรุ่นที่สมบูรณ์
+ label_me: ฉัน
+ label_board: สภากาแฟ
+ label_board_new: สร้างสภากาแฟ
+ label_board_plural: สภากาแฟ
+ label_topic_plural: หัวข้อ
+ label_message_plural: ข้อความ
+ label_message_last: ข้อความล่าสุด
+ label_message_new: เขียนข้อความใหม่
+ label_message_posted: ข้อความถูกเพิ่มแล้ว
+ label_reply_plural: ตอบกลับ
+ label_send_information: ส่งรายละเอียดของบัญชีให้ผู้ใช้
+ label_year: ปี
+ label_month: เดือน
+ label_week: สัปดาห์
+ label_date_from: จาก
+ label_date_to: ถึง
+ label_language_based: ขึ้นอยู่กับภาษาของผู้ใช้
+ label_sort_by: "เรียงโดย {{value}}"
+ label_send_test_email: ส่งจดหมายทดสอบ
+ label_feeds_access_key_created_on: "RSS access key สร้างเมื่อ {{value}} ที่ผ่านมา"
+ label_module_plural: ส่วนประกอบ
+ label_added_time_by: "เพิ่มโดย {{author}} {{age}} ที่ผ่านมา"
+ label_updated_time: "ปรับปรุง {{value}} ที่ผ่านมา"
+ label_jump_to_a_project: ไปที่โครงการ...
+ label_file_plural: แฟ้ม
+ label_changeset_plural: กลุ่มการเปลี่ยนแปลง
+ label_default_columns: สดมภ์เริ่มต้น
+ label_no_change_option: (ไม่เปลี่ยนแปลง)
+ label_bulk_edit_selected_issues: แก้ไขปัญหาที่เลือกทั้งหมด
+ label_theme: ชุดรูปแบบ
+ label_default: ค่าเริ่มต้น
+ label_search_titles_only: ค้นหาจากชื่อเรื่องเท่านั้น
+ label_user_mail_option_all: "ทุกๆ เหตุการณ์ในโครงการของฉัน"
+ label_user_mail_option_selected: "ทุกๆ เหตุการณ์ในโครงการที่เลือก..."
+ label_user_mail_option_none: "เฉพาะสิ่งที่ฉันเลือกหรือมีส่วนเกี่ยวข้อง"
+ label_user_mail_no_self_notified: "ฉันไม่ต้องการได้รับการแจ้งเตือนในสิ่งที่ฉันทำเอง"
+ label_registration_activation_by_email: เปิดบัญชีผ่านอีเมล์
+ label_registration_manual_activation: อนุมัติโดยผู้บริหารจัดการ
+ label_registration_automatic_activation: เปิดบัญชีอัตโนมัติ
+ label_display_per_page: "ต่อหน้า: {{value}}"
+ label_age: อายุ
+ label_change_properties: เปลี่ยนคุณสมบัติ
+ label_general: ทั่วๆ ไป
+ label_more: อื่น ๆ
+ label_scm: ตัวจัดการต้นฉบับ
+ label_plugins: ส่วนเสริม
+ label_ldap_authentication: การยืนยันตัวตนโดยใช้ LDAP
+ label_downloads_abbr: D/L
+ label_optional_description: รายละเอียดเพิ่มเติม
+ label_add_another_file: เพิ่มแฟ้มอื่นๆ
+ label_preferences: ค่าที่ชอบใจ
+ label_chronological_order: เรียงจากเก่าไปใหม่
+ label_reverse_chronological_order: เรียงจากใหม่ไปเก่า
+ label_planning: การวางแผน
+
+ button_login: เข้าระบบ
+ button_submit: จัดส่งข้อมูล
+ button_save: จัดเก็บ
+ button_check_all: เลือกทั้งหมด
+ button_uncheck_all: ไม่เลือกทั้งหมด
+ button_delete: ลบ
+ button_create: สร้าง
+ button_test: ทดสอบ
+ button_edit: แก้ไข
+ button_add: เพิ่ม
+ button_change: เปลี่ยนแปลง
+ button_apply: ประยุกต์ใช้
+ button_clear: ล้างข้อความ
+ button_lock: ล็อค
+ button_unlock: ยกเลิกการล็อค
+ button_download: ดาวน์โหลด
+ button_list: รายการ
+ button_view: มุมมอง
+ button_move: ย้าย
+ button_back: กลับ
+ button_cancel: ยกเลิก
+ button_activate: เปิดใช้
+ button_sort: จัดเรียง
+ button_log_time: บันทึกเวลา
+ button_rollback: ถอยกลับมาที่รุ่นนี้
+ button_watch: เฝ้าดู
+ button_unwatch: เลิกเฝ้าดู
+ button_reply: ตอบกลับ
+ button_archive: เก็บเข้าโกดัง
+ button_unarchive: เอาออกจากโกดัง
+ button_reset: เริ่มใหมท
+ button_rename: เปลี่ยนชื่อ
+ button_change_password: เปลี่ยนรหัสผ่าน
+ button_copy: คัดลอก
+ button_annotate: หมายเหตุประกอบ
+ button_update: ปรับปรุง
+ button_configure: ปรับแต่ง
+
+ status_active: เปิดใช้งานแล้ว
+ status_registered: รอการอนุมัติ
+ status_locked: ล็อค
+
+ text_select_mail_notifications: เลือกการกระทำที่ต้องการให้ส่งอีเมล์แจ้ง.
+ text_regexp_info: ตัวอย่าง ^[A-Z0-9]+$
+ text_min_max_length_info: 0 หมายถึงไม่จำกัด
+ text_project_destroy_confirmation: คุณแน่ใจไหมว่าต้องการลบโครงการและข้อมูลที่เกี่ยวข้่อง ?
+ text_subprojects_destroy_warning: "โครงการย่อย: {{value}} จะถูกลบด้วย."
+ text_workflow_edit: เลือกบทบาทและการติดตาม เพื่อแก้ไขลำดับงาน
+ text_are_you_sure: คุณแน่ใจไหม ?
+ text_tip_task_begin_day: งานที่เริ่มวันนี้
+ text_tip_task_end_day: งานที่จบวันนี้
+ text_tip_task_begin_end_day: งานที่เริ่มและจบวันนี้
+ text_project_identifier_info: 'ภาษาอังกฤษตัวเล็ก(a-z), ตัวเลข(0-9) และขีด (-) เท่านั้น.<br />เมื่อจัดเก็บแล้ว, ชื่อเฉพาะไม่สามารถเปลี่ยนแปลงได้'
+ text_caracters_maximum: "สูงสุด {{count}} ตัวอักษร."
+ text_caracters_minimum: "ต้องยาวอย่างน้อย {{count}} ตัวอักษร."
+ text_length_between: "ความยาวระหว่าง {{min}} ถึง {{max}} ตัวอักษร."
+ text_tracker_no_workflow: ไม่ได้บัญญัติลำดับงานสำหรับการติดตามนี้
+ text_unallowed_characters: ตัวอักษรต้องห้าม
+ text_comma_separated: ใส่ได้หลายค่า โดยคั่นด้วยลูกน้ำ( ,).
+ text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+ text_issue_added: "ปัญหา {{id}} ถูกแจ้งโดย {{author}}."
+ text_issue_updated: "ปัญหา {{id}} ถูกปรับปรุงโดย {{author}}."
+ text_wiki_destroy_confirmation: คุณแน่ใจหรือว่าต้องการลบ wiki นี้พร้อมทั้งเนี้อหา?
+ text_issue_category_destroy_question: "บางปัญหา ({{count}}) อยู่ในประเภทนี้. คุณต้องการทำอย่างไร ?"
+ text_issue_category_destroy_assignments: ลบประเภทนี้
+ text_issue_category_reassign_to: ระบุปัญหาในประเภทนี้
+ text_user_mail_option: "ในโครงการที่ไม่ได้เลือก, คุณจะได้รับการแจ้งเกี่ยวกับสิ่งที่คุณเฝ้าดูหรือมีส่วนเกี่ยวข้อง (เช่นปัญหาที่คุณแจ้งไว้หรือได้รับมอบหมาย)."
+ text_no_configuration_data: "บทบาท, การติดตาม, สถานะปัญหา และลำดับงานยังไม่ได้ถูกตั้งค่า.\nขอแนะนำให้โหลดค่าเริ่มต้น. คุณสามารถแก้ไขค่าได้หลังจากโหลดแล้ว."
+ text_load_default_configuration: โหลดค่าเริ่มต้น
+ text_status_changed_by_changeset: "ประยุกต์ใช้ในกลุ่มการเปลี่ยนแปลง {{value}}."
+ text_issues_destroy_confirmation: 'คุณแน่ใจไหมว่าต้องการลบปัญหา(ทั้งหลาย)ที่เลือกไว้?'
+ text_select_project_modules: 'เลือกส่วนประกอบที่ต้องการใช้งานสำหรับโครงการนี้:'
+ text_default_administrator_account_changed: ค่าเริ่มต้นของบัญชีผู้บริหารจัดการถูกเปลี่ยนแปลง
+ text_file_repository_writable: ที่เก็บต้นฉบับสามารถเขียนได้
+ text_rmagick_available: RMagick มีให้ใช้ (เป็นตัวเลือก)
+ text_destroy_time_entries_question: "{{hours}} ชั่วโมงที่ถูกแจ้งในปัญหานี้จะโดนลบ. คุณต้องการทำอย่างไร?"
+ text_destroy_time_entries: ลบเวลาที่รายงานไว้
+ text_assign_time_entries_to_project: ระบุเวลาที่ใช้ในโครงการนี้
+ text_reassign_time_entries: 'ระบุเวลาที่ใช้ในโครงการนี่อีกครั้ง:'
+
+ default_role_manager: ผู้จัดการ
+ default_role_developper: ผู้พัฒนา
+ default_role_reporter: ผู้รายงาน
+ default_tracker_bug: บั๊ก
+ default_tracker_feature: ลักษณะเด่น
+ default_tracker_support: สนับสนุน
+ default_issue_status_new: เกิดขึ้น
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: ดำเนินการ
+ default_issue_status_feedback: รอคำตอบ
+ default_issue_status_closed: จบ
+ default_issue_status_rejected: ยกเลิก
+ default_doc_category_user: เอกสารของผู้ใช้
+ default_doc_category_tech: เอกสารทางเทคนิค
+ default_priority_low: ต่ำ
+ default_priority_normal: ปกติ
+ default_priority_high: สูง
+ default_priority_urgent: เร่งด่วน
+ default_priority_immediate: ด่วนมาก
+ default_activity_design: ออกแบบ
+ default_activity_development: พัฒนา
+
+ enumeration_issue_priorities: ความสำคัญของปัญหา
+ enumeration_doc_categories: ประเภทเอกสาร
+ enumeration_activities: กิจกรรม (ใช้ในการติดตามเวลา)
+ label_and_its_subprojects: "{{value}} and its subprojects"
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ text_user_wrote: "{{value}} wrote:"
+ label_duplicated_by: duplicated by
+ setting_enabled_scm: Enabled SCM
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: API key
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: Parent page
+ label_issue_watchers: Watchers
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: Quote
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: renamed
+ label_copied: copied
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: Example
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Turkish translations for Ruby on Rails
+# by Ozgun Ataman (ozataman@gmail.com)
+
+tr:
+ locale:
+ native_name: Türkçe
+ address_separator: " "
+ date:
+ formats:
+ default: "%d.%m.%Y"
+ numeric: "%d.%m.%Y"
+ short: "%e %b"
+ long: "%e %B %Y, %A"
+ only_day: "%e"
+
+ day_names: [Pazar, Pazartesi, Salı, Çarşamba, Perşembe, Cuma, Cumartesi]
+ abbr_day_names: [Pzr, Pzt, Sal, Çrş, Prş, Cum, Cts]
+ month_names: [~, Ocak, Şubat, Mart, Nisan, Mayıs, Haziran, Temmuz, Ağustos, Eylül, Ekim, Kasım, Aralık]
+ abbr_month_names: [~, Oca, Şub, Mar, Nis, May, Haz, Tem, Ağu, Eyl, Eki, Kas, Ara]
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%a %d.%b.%y %H:%M"
+ numeric: "%d.%b.%y %H:%M"
+ short: "%e %B, %H:%M"
+ long: "%e %B %Y, %A, %H:%M"
+ time: "%H:%M"
+
+ am: "öğleden önce"
+ pm: "öğleden sonra"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: 'yarım dakika'
+ less_than_x_seconds:
+ zero: '1 saniyeden az'
+ one: '1 saniyeden az'
+ other: '{{count}} saniyeden az'
+ x_seconds:
+ one: '1 saniye'
+ other: '{{count}} saniye'
+ less_than_x_minutes:
+ zero: '1 dakikadan az'
+ one: '1 dakikadan az'
+ other: '{{count}} dakikadan az'
+ x_minutes:
+ one: '1 dakika'
+ other: '{{count}} dakika'
+ about_x_hours:
+ one: '1 saat civarında'
+ other: '{{count}} saat civarında'
+ x_days:
+ one: '1 gün'
+ other: '{{count}} gün'
+ about_x_months:
+ one: '1 ay civarında'
+ other: '{{count}} ay civarında'
+ x_months:
+ one: '1 ay'
+ other: '{{count}} ay'
+ about_x_years:
+ one: '1 yıl civarında'
+ other: '{{count}} yıl civarında'
+ over_x_years:
+ one: '1 yıldan fazla'
+ other: '{{count}} yıldan fazla'
+
+ number:
+ format:
+ precision: 2
+ separator: ','
+ delimiter: '.'
+ currency:
+ format:
+ unit: 'TRY'
+ format: '%n%u'
+ separator: ','
+ delimiter: '.'
+ precision: 2
+ percentage:
+ format:
+ delimiter: '.'
+ separator: ','
+ precision: 2
+ precision:
+ format:
+ delimiter: '.'
+ separator: ','
+ human:
+ format:
+ delimiter: '.'
+ separator: ','
+ precision: 2
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ sentence_connector: "ve"
+ skip_last_comma: true
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "{{model}} girişi kaydedilemedi: 1 hata."
+ other: "{{model}} girişi kadedilemedi: {{count}} hata."
+ body: "Lütfen aşağıdaki hataları düzeltiniz:"
+
+ messages:
+ inclusion: "kabul edilen bir kelime değil"
+ exclusion: "kullanılamaz"
+ invalid: "geçersiz"
+ confirmation: "teyidi uyuşmamakta"
+ accepted: "kabul edilmeli"
+ empty: "doldurulmalı"
+ blank: "doldurulmalı"
+ too_long: "çok uzun (en fazla {{count}} karakter)"
+ too_short: "çok kısa (en az {{count}} karakter)"
+ wrong_length: "yanlış uzunlukta (tam olarak {{count}} karakter olmalı)"
+ taken: "hali hazırda kullanılmakta"
+ not_a_number: "geçerli bir sayı değil"
+ greater_than: "{{count}} sayısından büyük olmalı"
+ greater_than_or_equal_to: "{{count}} sayısına eşit veya büyük olmalı"
+ equal_to: "tam olarak {{count}} olmalı"
+ less_than: "{{count}} sayısından küçük olmalı"
+ less_than_or_equal_to: "{{count}} sayısına eşit veya küçük olmalı"
+ odd: "tek olmalı"
+ even: "çift olmalı"
+ greater_than_start_date: "başlangıç tarihinden büyük olmalı"
+ not_same_project: "aynı projeye ait değil"
+ circular_dependency: "Bu ilişki döngüsel bağımlılık meydana getirecektir"
+ models:
+
+ actionview_instancetag_blank_option: Lütfen Seçin
+
+ general_text_No: 'Hayır'
+ general_text_Yes: 'Evet'
+ general_text_no: 'hayır'
+ general_text_yes: 'evet'
+ general_lang_name: 'Türkçe'
+ general_csv_separator: ','
+ general_csv_encoding: ISO-8859-1
+ general_pdf_encoding: ISO-8859-1
+ general_first_day_of_week: '7'
+
+ notice_account_updated: Hesap başarıyla güncelleştirildi.
+ notice_account_invalid_creditentials: Geçersiz kullanıcı ya da parola
+ notice_account_password_updated: Parola başarıyla güncellendi.
+ notice_account_wrong_password: Yanlış parola
+ notice_account_register_done: Hesap başarıyla oluşturuldu. Hesabınızı etkinleştirmek için, size gönderilen e-postadaki bağlantıya tıklayın.
+ notice_account_unknown_email: Tanınmayan kullanıcı.
+ notice_can_t_change_password: Bu hesap harici bir denetim kaynağı kullanıyor. Parolayı değiştirmek mümkün değil.
+ notice_account_lost_email_sent: Yeni parola seçme talimatlarını içeren e-postanız gönderildi.
+ notice_account_activated: Hesabınız etkinleştirildi. Şimdi giriş yapabilirsiniz.
+ notice_successful_create: Başarıyla oluşturuldu.
+ notice_successful_update: Başarıyla güncellendi.
+ notice_successful_delete: Başarıyla silindi.
+ notice_successful_connection: Bağlantı başarılı.
+ notice_file_not_found: Erişmek istediğiniz sayfa mevcut değil ya da kaldırılmış.
+ notice_locking_conflict: Veri başka bir kullanıcı tarafından güncellendi.
+ notice_not_authorized: Bu sayfaya erişme yetkiniz yok.
+ notice_email_sent: "E-posta gönderildi {{value}}"
+ notice_email_error: "E-posta gönderilirken bir hata oluştu ({{value}})"
+ notice_feeds_access_key_reseted: RSS erişim anahtarınız sıfırlandı.
+ notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
+ notice_no_issue_selected: "Seçili ileti yok! Lütfen, düzenlemek istediğiniz iletileri işaretleyin."
+ notice_account_pending: "Hesabınız oluşturuldu ve yönetici onayı bekliyor."
+ notice_default_data_loaded: Varasayılan konfigürasyon başarılıyla yüklendi.
+
+ error_can_t_load_default_data: "Varsayılan konfigürasyon yüklenemedi: {{value}}"
+ error_scm_not_found: "Depoda, giriş ya da revizyon yok."
+ error_scm_command_failed: "Depoya erişmeye çalışırken bir hata meydana geldi: {{value}}"
+ error_scm_annotate: "Giriş mevcut değil veya izah edilemedi."
+ error_issue_not_found_in_project: 'İleti bilgisi bulunamadı veya bu projeye ait değil'
+
+ mail_subject_lost_password: "Parolanız {{value}}"
+ mail_body_lost_password: 'Parolanızı değiştirmek için, aşağıdaki bağlantıya tıklayın:'
+ mail_subject_register: "Your {{value}} hesap aktivasyonu"
+ mail_body_register: 'Hesabınızı etkinleştirmek için, aşağıdaki bağlantıya tıklayın:'
+ mail_body_account_information_external: "Hesabınızı {{value}} giriş yapmak için kullanabilirsiniz."
+ mail_body_account_information: Hesap bilgileriniz
+ mail_subject_account_activation_request: "{{value}} hesabı etkinleştirme isteği"
+ mail_body_account_activation_request: "Yeni bir kullanıcı ({{value}}) kaydedildi. Hesap onaylanmayı bekliyor:"
+
+ gui_validation_error: 1 hata
+ gui_validation_error_plural: "{{count}} hata"
+
+ field_name: İsim
+ field_description: Açıklama
+ field_summary: Özet
+ field_is_required: Gerekli
+ field_firstname: Ad
+ field_lastname: Soyad
+ field_mail: E-Posta
+ field_filename: Dosya
+ field_filesize: Boyut
+ field_downloads: İndirilenler
+ field_author: Yazar
+ field_created_on: Oluşturuldu
+ field_updated_on: Güncellendi
+ field_field_format: Biçim
+ field_is_for_all: Tüm projeler için
+ field_possible_values: Mümkün değerler
+ field_regexp: Düzenli ifadeler
+ field_min_length: En az uzunluk
+ field_max_length: En çok uzunluk
+ field_value: Değer
+ field_category: Kategori
+ field_title: Başlık
+ field_project: Proje
+ field_issue: İleti
+ field_status: Durum
+ field_notes: Notlar
+ field_is_closed: İleti kapatıldı
+ field_is_default: Varsayılan Değer
+ field_tracker: Takipçi
+ field_subject: Konu
+ field_due_date: Bitiş Tarihi
+ field_assigned_to: Atanan
+ field_priority: Öncelik
+ field_fixed_version: Hedef Version
+ field_user: Kullanıcı
+ field_role: Rol
+ field_homepage: Anasayfa
+ field_is_public: Genel
+ field_parent: 'Üst proje: '
+ field_is_in_chlog: Değişim günlüğünde gösterilen iletiler
+ field_is_in_roadmap: Yol haritasında gösterilen iletiler
+ field_login: Giriş
+ field_mail_notification: E-posta uyarıları
+ field_admin: Yönetici
+ field_last_login_on: Son Bağlantı
+ field_language: Dil
+ field_effective_date: Tarih
+ field_password: Parola
+ field_new_password: Yeni Parola
+ field_password_confirmation: Onay
+ field_version: Versiyon
+ field_type: Tip
+ field_host: Host
+ field_port: Port
+ field_account: Hesap
+ field_base_dn: Base DN
+ field_attr_login: Giriş Niteliği
+ field_attr_firstname: Ad Niteliği
+ field_attr_lastname: Soyad Niteliği
+ field_attr_mail: E-Posta Niteliği
+ field_onthefly: Anında kullanıcı oluşturma
+ field_start_date: Başlangıç
+ field_done_ratio: % tamamlandı
+ field_auth_source: Kimlik Denetim Modu
+ field_hide_mail: E-posta adresimi gizle
+ field_comments: Açıklama
+ field_url: URL
+ field_start_page: Başlangıç Sayfası
+ field_subproject: Alt Proje
+ field_hours: Saatler
+ field_activity: Faaliyet
+ field_spent_on: Tarih
+ field_identifier: Tanımlayıcı
+ field_is_filter: filtre olarak kullanılmış
+ field_issue_to: İlişkili ileti
+ field_delay: Gecikme
+ field_assignable: Bu role atanabilecek iletiler
+ field_redirect_existing_links: Mevcut bağlantıları tekrar yönlendir
+ field_estimated_hours: Kalan zaman
+ field_column_names: Sütunlar
+ field_time_zone: Saat dilimi
+ field_searchable: Aranabilir
+ field_default_value: Varsayılan değer
+ field_comments_sorting: Açıklamaları göster
+
+ setting_app_title: Uygulama Bağlığı
+ setting_app_subtitle: Uygulama alt başlığı
+ setting_welcome_text: Hoşgeldin Mesajı
+ setting_default_language: Varsayılan Dil
+ setting_login_required: Kimlik denetimi gerekli mi
+ setting_self_registration: Otomatik kayıt
+ setting_attachment_max_size: Maksimum Ek boyutu
+ setting_issues_export_limit: İletilerin dışa aktarılma sınırı
+ setting_mail_from: Gönderici e-posta adresi
+ setting_bcc_recipients: Alıcıları birbirinden gizle (bcc)
+ setting_host_name: Host adı
+ setting_text_formatting: Metin biçimi
+ setting_wiki_compression: Wiki geçmişini sıkıştır
+ setting_feeds_limit: Haber yayını içerik limiti
+ setting_default_projects_public: Yeni projeler varsayılan olarak herkese açık
+ setting_autofetch_changesets: Otomatik gönderi al
+ setting_sys_api_enabled: Depo yönetimi için WS'yi etkinleştir
+ setting_commit_ref_keywords: Başvuru Kelimeleri
+ setting_commit_fix_keywords: Sabitleme kelimeleri
+ setting_autologin: Otomatik Giriş
+ setting_date_format: Tarih Formati
+ setting_time_format: Zaman Formatı
+ setting_cross_project_issue_relations: Çapraz-Proje ileti ilişkilendirmesine izin ver
+ setting_issue_list_default_columns: İleti listesinde gösterilen varsayılan sütunlar
+ setting_repositories_encodings: Depo dil kodlaması
+ setting_emails_footer: E-posta dip not
+ setting_protocol: Protokol
+ setting_per_page_options: Sayfa seçenekleri başına nesneler
+ setting_user_format: Kullanıcı gösterim formatı
+ setting_activity_days_default: Proje Faaliyetlerinde gösterilen gün sayısı
+ setting_display_subprojects_issues: Varsayılan olarak ana projenin ileti listesinde alt proje iletilerini göster
+
+ project_module_issue_tracking: İleti Takibi
+ project_module_time_tracking: Zaman Takibi
+ project_module_news: Haberler
+ project_module_documents: Belgeler
+ project_module_files: Dosyalar
+ project_module_wiki: Wiki
+ project_module_repository: Depo
+ project_module_boards: Tartışma Alanı
+
+ label_user: Kullanıcı
+ label_user_plural: Kullanıcılar
+ label_user_new: Yeni Kullanıcı
+ label_project: Proje
+ label_project_new: Yeni proje
+ label_project_plural: Projeler
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Tüm Projeler
+ label_project_latest: En son projeler
+ label_issue: İleti
+ label_issue_new: Yeni İleti
+ label_issue_plural: İletiler
+ label_issue_view_all: Tüm iletileri izle
+ label_issues_by: "{{value}} tarafından gönderilmiş iletiler"
+ label_issue_added: İleti eklendi
+ label_issue_updated: İleti güncellendi
+ label_document: Belge
+ label_document_new: Yeni belge
+ label_document_plural: Belgeler
+ label_document_added: Belge eklendi
+ label_role: Rol
+ label_role_plural: Roller
+ label_role_new: Yeni rol
+ label_role_and_permissions: Roller ve izinler
+ label_member: Üye
+ label_member_new: Yeni üye
+ label_member_plural: Üyeler
+ label_tracker: Takipçi
+ label_tracker_plural: Takipçiler
+ label_tracker_new: Yeni takipçi
+ label_workflow: İş akışı
+ label_issue_status: İleti durumu
+ label_issue_status_plural: İleti durumuları
+ label_issue_status_new: Yeni durum
+ label_issue_category: İleti kategorisi
+ label_issue_category_plural: İleti kategorileri
+ label_issue_category_new: Yeni kategori
+ label_custom_field: Özel alan
+ label_custom_field_plural: Özel alanlar
+ label_custom_field_new: Yeni özel alan
+ label_enumerations: Numaralandırmalar
+ label_enumeration_new: Yeni değer
+ label_information: Bilgi
+ label_information_plural: Bilgi
+ label_please_login: Lütfen giriş yapın
+ label_register: Kayıt
+ label_password_lost: Parolamı unuttum?
+ label_home: Anasayfa
+ label_my_page: Kişisel Sayfam
+ label_my_account: Hesabım
+ label_my_projects: Projelerim
+ label_administration: Yönetim
+ label_login: Gir
+ label_logout: Çıkış
+ label_help: Yardım
+ label_reported_issues: Rapor edilmiş iletiler
+ label_assigned_to_me_issues: Bana atanmış iletiler
+ label_last_login: Son bağlantı
+ label_registered_on: Kayıtlı
+ label_activity: Faaliyet
+ label_overall_activity: Tüm aktiviteler
+ label_new: Yeni
+ label_logged_as: "Kullanıcı :"
+ label_environment: Çevre
+ label_authentication: Kimlik Denetimi
+ label_auth_source: Kimlik Denetim Modu
+ label_auth_source_new: Yeni Denetim Modu
+ label_auth_source_plural: Denetim Modları
+ label_subproject_plural: Alt Projeler
+ label_min_max_length: Min - Maks uzunluk
+ label_list: Liste
+ label_date: Tarih
+ label_integer: Tam sayı
+ label_float: Noktalı sayı
+ label_boolean: Boolean
+ label_string: Metin
+ label_text: Uzun Metin
+ label_attribute: Nitelik
+ label_attribute_plural: Nitelikler
+ label_download: "{{count}} indirilen"
+ label_download_plural: "{{count}} indirilen"
+ label_no_data: Gösterilecek veri yok
+ label_change_status: Değişim Durumu
+ label_history: Geçmiş
+ label_attachment: Dosya
+ label_attachment_new: Yeni Dosya
+ label_attachment_delete: Dosyayı Sil
+ label_attachment_plural: Dosyalar
+ label_file_added: Eklenen Dosyalar
+ label_report: Rapor
+ label_report_plural: Raporlar
+ label_news: Haber
+ label_news_new: Haber ekle
+ label_news_plural: Haber
+ label_news_latest: Son Haberler
+ label_news_view_all: Tüm haberleri oku
+ label_news_added: Haber eklendi
+ label_change_log: Değişim Günlüğü
+ label_settings: Ayarlar
+ label_overview: Genel
+ label_version: Versiyon
+ label_version_new: Yeni versiyon
+ label_version_plural: Versiyonlar
+ label_confirmation: Doğrulamama
+ label_export_to: 'Diğer uygun kaynaklar:'
+ label_read: Oku...
+ label_public_projects: Genel Projeler
+ label_open_issues: açık
+ label_open_issues_plural: açık
+ label_closed_issues: kapalı
+ label_closed_issues_plural: kapalı
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Toplam
+ label_permissions: İzinler
+ label_current_status: Mevcut Durum
+ label_new_statuses_allowed: Yeni durumlara izin verildi
+ label_all: Hepsi
+ label_none: Hiçbiri
+ label_nobody: Hiçkimse
+ label_next: Sonraki
+ label_previous: Önceki
+ label_used_by: 'Kullanan: '
+ label_details: Detaylar
+ label_add_note: Bir not ekle
+ label_per_page: Sayfa başına
+ label_calendar: Takvim
+ label_months_from: aylardan itibaren
+ label_gantt: Gantt
+ label_internal: Dahili
+ label_last_changes: "Son {{count}} değişiklik"
+ label_change_view_all: Tüm Değişiklikleri gör
+ label_personalize_page: Bu sayfayı kişiselleştir
+ label_comment: Açıklama
+ label_comment_plural: Açıklamalar
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Açıklama Ekle
+ label_comment_added: Açıklama Eklendi
+ label_comment_delete: Açıklamaları sil
+ label_query: Özel Sorgu
+ label_query_plural: Özel Sorgular
+ label_query_new: Yeni Sorgu
+ label_filter_add: Filtre ekle
+ label_filter_plural: Filtreler
+ label_equals: Eşit
+ label_not_equals: Eşit değil
+ label_in_less_than: küçüktür
+ label_in_more_than: büyüktür
+ label_in: içinde
+ label_today: bugün
+ label_all_time: Tüm Zamanlar
+ label_yesterday: Dün
+ label_this_week: Bu hafta
+ label_last_week: Geçen hafta
+ label_last_n_days: "Son {{count}} gün"
+ label_this_month: Bu ay
+ label_last_month: Geçen ay
+ label_this_year: Bu yıl
+ label_date_range: Tarih aralığı
+ label_less_than_ago: günler öncesinden az
+ label_more_than_ago: günler öncesinden fazla
+ label_ago: gün önce
+ label_contains: içeriyor
+ label_not_contains: içermiyor
+ label_day_plural: Günler
+ label_repository: Depo
+ label_repository_plural: Depolar
+ label_browse: Tara
+ label_modification: "{{count}} değişim"
+ label_modification_plural: "{{count}} değişim"
+ label_revision: Revizyon
+ label_revision_plural: Revizyonlar
+ label_associated_revisions: Birleştirilmiş revizyonlar
+ label_added: eklendi
+ label_modified: güncellendi
+ label_deleted: silindi
+ label_latest_revision: En son revizyon
+ label_latest_revision_plural: En son revizyonlar
+ label_view_revisions: Revizyonları izle
+ label_max_size: En büyük boyut
+ label_sort_highest: Üste taşı
+ label_sort_higher: Yukarı taşı
+ label_sort_lower: Aşağı taşı
+ label_sort_lowest: Dibe taşı
+ label_roadmap: Yol Haritası
+ label_roadmap_due_in: "Due in {{value}}"
+ label_roadmap_overdue: "{{value}} geç"
+ label_roadmap_no_issues: Bu versiyon için ileti yok
+ label_search: Ara
+ label_result_plural: Sonuçlar
+ label_all_words: Tüm Kelimeler
+ label_wiki: Wiki
+ label_wiki_edit: Wiki düzenleme
+ label_wiki_edit_plural: Wiki düzenlemeleri
+ label_wiki_page: Wiki sayfası
+ label_wiki_page_plural: Wiki sayfaları
+ label_index_by_title: Başlığa göre diz
+ label_index_by_date: Tarihe göre diz
+ label_current_version: Güncel versiyon
+ label_preview: Önizleme
+ label_feed_plural: Beslemeler
+ label_changes_details: Bütün değişikliklerin detayları
+ label_issue_tracking: İleti Takibi
+ label_spent_time: Harcanan zaman
+ label_f_hour: "{{value}} saat"
+ label_f_hour_plural: "{{value}} saat"
+ label_time_tracking: Zaman Takibi
+ label_change_plural: Değişiklikler
+ label_statistics: İstatistikler
+ label_commits_per_month: Aylık teslim
+ label_commits_per_author: Yazar başına teslim
+ label_view_diff: Farkları izle
+ label_diff_inline: satır içi
+ label_diff_side_by_side: Yan yana
+ label_options: Tercihler
+ label_copy_workflow_from: İşakışı kopyala
+ label_permissions_report: İzin raporu
+ label_watched_issues: İzlenmiş iletiler
+ label_related_issues: İlişkili iletiler
+ label_applied_status: uygulanmış iletiler
+ label_loading: Yükleniyor...
+ label_relation_new: Yeni ilişki
+ label_relation_delete: İlişkiyi sil
+ label_relates_to: ilişkili
+ label_duplicates: yinelenmiş
+ label_blocks: Engeller
+ label_blocked_by: Engelleyen
+ label_precedes: önce gelir
+ label_follows: sonra gelir
+ label_end_to_start: sondan başa
+ label_end_to_end: sondan sona
+ label_start_to_start: baştan başa
+ label_start_to_end: baştan sona
+ label_stay_logged_in: Sürekli bağlı kal
+ label_disabled: Devredışı
+ label_show_completed_versions: Tamamlanmış versiyonları göster
+ label_me: Ben
+ label_board: Tartışma Alanı
+ label_board_new: Yeni alan
+ label_board_plural: Tartışma alanları
+ label_topic_plural: Konular
+ label_message_plural: Mesajlar
+ label_message_last: Son mesaj
+ label_message_new: Yeni mesaj
+ label_message_posted: Mesaj eklendi
+ label_reply_plural: Cevaplar
+ label_send_information: Hesap bilgisini kullanıcıya gönder
+ label_year: Yıl
+ label_month: Ay
+ label_week: Hafta
+ label_date_from: Başlangıç
+ label_date_to: Bitiş
+ label_language_based: Kullanıcı diline istinaden
+ label_sort_by: "{{value}} göre sırala"
+ label_send_test_email: Test e-postası gönder
+ label_feeds_access_key_created_on: "RSS erişim anahtarı {{value}} önce oluşturuldu"
+ label_module_plural: Modüller
+ label_added_time_by: "{{author}} tarafından {{age}} önce eklendi"
+ label_updated_time: "{{value}} önce güncellendi"
+ label_jump_to_a_project: Projeye git...
+ label_file_plural: Dosyalar
+ label_changeset_plural: Değişiklik Listeleri
+ label_default_columns: Varsayılan Sütunlar
+ label_no_change_option: (Değişiklik yok)
+ label_bulk_edit_selected_issues: Seçili iletileri toplu düzenle
+ label_theme: Tema
+ label_default: Varsayılan
+ label_search_titles_only: Sadece başlıkları ara
+ label_user_mail_option_all: "Tüm projelerimdeki herhangi bir olay için"
+ label_user_mail_option_selected: "Sadece seçili projelerdeki herhangi bir olay için..."
+ label_user_mail_option_none: "Sadece dahil olduğum ya da izlediklerim için"
+ label_user_mail_no_self_notified: "Kendi yaptığım değişikliklerden haberdar olmak istemiyorum"
+ label_registration_activation_by_email: e-posta ile hesap etkinleştirme
+ label_registration_manual_activation: Elle hesap etkinleştirme
+ label_registration_automatic_activation: Otomatik hesap etkinleştirme
+ label_display_per_page: "Sayfa başına: {{value}}"
+ label_age: Yaş
+ label_change_properties: Özellikleri değiştir
+ label_general: Genel
+ label_more: Daha fazla
+ label_scm: KKY
+ label_plugins: Eklentiler
+ label_ldap_authentication: LDAP Denetimi
+ label_downloads_abbr: D/L
+ label_optional_description: İsteğe bağlı açıklama
+ label_add_another_file: Bir dosya daha ekle
+ label_preferences: Tercihler
+ label_chronological_order: Tarih sırasına göre
+ label_reverse_chronological_order: Ters tarih sırasına göre
+ label_planning: Planlanıyor
+
+ button_login: Giriş
+ button_submit: Gönder
+ button_save: Kaydet
+ button_check_all: Hepsini işaretle
+ button_uncheck_all: Tüm işaretleri kaldır
+ button_delete: Sil
+ button_create: Oluştur
+ button_test: Sına
+ button_edit: Düzenle
+ button_add: Ekle
+ button_change: Değiştir
+ button_apply: Uygula
+ button_clear: Temizle
+ button_lock: Kilitle
+ button_unlock: Kilidi aç
+ button_download: İndir
+ button_list: Listele
+ button_view: Bak
+ button_move: Taşı
+ button_back: Geri
+ button_cancel: İptal
+ button_activate: Etkinleştir
+ button_sort: Sırala
+ button_log_time: Günlük zamanı
+ button_rollback: Bu versiyone geri al
+ button_watch: İzle
+ button_unwatch: İzlemeyi iptal et
+ button_reply: Cevapla
+ button_archive: Arşivle
+ button_unarchive: Arşivlemeyi kaldır
+ button_reset: Sıfırla
+ button_rename: Yeniden adlandır
+ button_change_password: Parolayı değiştir
+ button_copy: Kopyala
+ button_annotate: Revizyon geçmişine göre göster
+ button_update: Güncelle
+ button_configure: Yapılandır
+
+ status_active: faal
+ status_registered: kayıtlı
+ status_locked: kilitli
+
+ text_select_mail_notifications: Gönderilecek e-posta uyarısına göre hareketi seçin.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 sınırlama yok demektir
+ text_project_destroy_confirmation: Bu projeyi ve bağlantılı verileri silmek istediğinizden emin misiniz?
+ text_subprojects_destroy_warning: "Ayrıca {{value}} alt proje silinecek."
+ text_workflow_edit: İşakışını düzenlemek için bir rol ve takipçi seçin
+ text_are_you_sure: Emin misiniz ?
+ text_tip_task_begin_day: Bugün başlayan görevler
+ text_tip_task_end_day: Bugün sona eren görevler
+ text_tip_task_begin_end_day: Bugün başlayan ve sona eren görevler
+ text_project_identifier_info: 'Küçük harfler (a-z), sayılar ve noktalar kabul edilir.<br />Bir kere kaydedildiğinde,tanımlayıcı değiştirilemez.'
+ text_caracters_maximum: "En çok {{count}} karakter."
+ text_caracters_minimum: "En az {{count}} karakter uzunluğunda olmalı."
+ text_length_between: "{{min}} ve {{max}} karakterleri arasındaki uzunluk."
+ text_tracker_no_workflow: Bu takipçi için işakışı tanımlanmamış
+ text_unallowed_characters: Yasaklı karakterler
+ text_comma_separated: Çoklu değer uygundur(Virgül ile ayrılmış).
+ text_issues_ref_in_commit_messages: Teslim mesajlarındaki iletileri çözme ve başvuruda bulunma
+ text_issue_added: "İleti {{id}}, {{author}} tarafından rapor edildi."
+ text_issue_updated: "İleti {{id}}, {{author}} tarafından güncellendi."
+ text_wiki_destroy_confirmation: bu wikiyi ve tüm içeriğini silmek istediğinizden emin misiniz?
+ text_issue_category_destroy_question: "Bazı iletiler ({{count}}) bu kategoriye atandı. Ne yapmak istersiniz?"
+ text_issue_category_destroy_assignments: Kategori atamalarını kaldır
+ text_issue_category_reassign_to: İletileri bu kategoriye tekrar ata
+ text_user_mail_option: "Seçili olmayan projeler için, sadece dahil olduğunuz ya da izlediğiniz öğeler hakkında uyarılar alacaksınız (örneğin,yazarı veya atandığınız iletiler)."
+ text_no_configuration_data: "Roller, takipçiler, ileti durumları ve işakışı henüz yapılandırılmadı.\nVarsayılan yapılandırılmanın yüklenmesi şiddetle tavsiye edilir. Bir kez yüklendiğinde yapılandırmayı değiştirebileceksiniz."
+ text_load_default_configuration: Varsayılan yapılandırmayı yükle
+ text_status_changed_by_changeset: "Değişiklik listesi {{value}} içinde uygulandı."
+ text_issues_destroy_confirmation: 'Seçili iletileri silmek istediğinizden emin misiniz ?'
+ text_select_project_modules: 'Bu proje için etkinleştirmek istediğiniz modülleri seçin:'
+ text_default_administrator_account_changed: Varsayılan yönetici hesabı değişti
+ text_file_repository_writable: Dosya deposu yazılabilir
+ text_rmagick_available: RMagick Kullanılabilir (isteğe bağlı)
+ text_destroy_time_entries_question: Silmek üzere olduğunuz iletiler üzerine {{hours}} saat raporlandı.Ne yapmak istersiniz ?
+ text_destroy_time_entries: Raporlanmış saatleri sil
+ text_assign_time_entries_to_project: Raporlanmış saatleri projeye ata
+ text_reassign_time_entries: 'Raporlanmış saatleri bu iletiye tekrar ata:'
+
+ default_role_manager: Yönetici
+ default_role_developper: Geliştirici
+ default_role_reporter: Raporlayıcı
+ default_tracker_bug: Hata
+ default_tracker_feature: ÖZellik
+ default_tracker_support: Destek
+ default_issue_status_new: Yeni
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Çözüldü
+ default_issue_status_feedback: Geribildirim
+ default_issue_status_closed: Kapatıldı
+ default_issue_status_rejected: Reddedildi
+ default_doc_category_user: Kullanıcı Dökümantasyonu
+ default_doc_category_tech: Teknik Dökümantasyon
+ default_priority_low: Düşük
+ default_priority_normal: Normal
+ default_priority_high: Yüksek
+ default_priority_urgent: Acil
+ default_priority_immediate: Derhal
+ default_activity_design: Tasarım
+ default_activity_development: Geliştirim
+
+ enumeration_issue_priorities: İleti önceliği
+ enumeration_doc_categories: Belge Kategorileri
+ enumeration_activities: Faaliyetler (zaman takibi)
+ button_quote: Quote
+ setting_enabled_scm: Enabled SCM
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ field_parent_title: Parent page
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ label_issue_watchers: Watchers
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ label_duplicated_by: duplicated by
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ text_user_wrote: "{{value}} wrote:"
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ label_and_its_subprojects: "{{value}} and its subprojects"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ setting_mail_handler_api_key: API key
+ setting_commit_logs_encoding: Commit messages encoding
+ general_csv_decimal_separator: '.'
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: renamed
+ label_copied: copied
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: Example
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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").
+ label_group_plural: Groups
+ label_group: Group
+ label_group_new: New group
+ label_time_entry_plural: Spent time
+ text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
+ text_journal_set_to: "{{label}} set to {{value}}"
+ text_journal_deleted: "{{label}} deleted ({{old}})"
+ 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
--- /dev/null
+uk:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ number:
+ human:
+ format:
+ precision: 1
+ delimiter: ""
+ storage_units:
+ format: "%n %u"
+ units:
+ kb: KB
+ tb: TB
+ gb: GB
+ byte:
+ one: Byte
+ other: Bytes
+ mb: MB
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
+ skip_last_comma: false
+
+ activerecord:
+ errors:
+ messages:
+ inclusion: "немає в списку"
+ exclusion: "зарезервовано"
+ invalid: "невірне"
+ confirmation: "не збігається з підтвердженням"
+ accepted: "необхідно прийняти"
+ empty: "не може бути порожнім"
+ blank: "не може бути незаповненим"
+ too_long: "дуже довге"
+ too_short: "дуже коротке"
+ wrong_length: "не відповідає довжині"
+ taken: "вже використовується"
+ not_a_number: "не є числом"
+ not_a_date: "є недійсною датою"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ greater_than_start_date: "повинна бути пізніша за дату початку"
+ not_same_project: "не відносяться до одного проекту"
+ circular_dependency: "Такий зв'язок приведе до циклічної залежності"
+
+ actionview_instancetag_blank_option: Оберіть
+
+ general_text_No: 'Ні'
+ general_text_Yes: 'Так'
+ general_text_no: 'Ні'
+ general_text_yes: 'Так'
+ general_lang_name: 'Ukrainian (Українська)'
+ 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_scm_error: Запису та/або виправлення немає в репозиторії.
+ notice_not_authorized: У вас немає прав для відвідини даної сторінки.
+ notice_email_sent: "Відправлено листа {{value}}"
+ notice_email_error: "Під час відправки листа відбулася помилка ({{value}})"
+ notice_feeds_access_key_reseted: Ваш ключ доступу RSS було скинуто.
+ notice_failed_to_save_issues: "Не вдалося зберегти {{count}} пункт(ів) з {{total}} вибраних: {{ids}}."
+ notice_no_issue_selected: "Не вибрано жодної задачі! Будь ласка, відзначте задачу, яку ви хочете відредагувати."
+ notice_account_pending: "Ваш обліковий запис створено і він чекає на підтвердження адміністратором."
+
+ 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}}) зареєструвався. Його обліковий запис чекає на ваше підтвердження:"
+
+ gui_validation_error: 1 помилка
+ gui_validation_error_plural: "{{count}} помилки(ок)"
+
+ field_name: Ім'я
+ field_description: Опис
+ field_summary: Короткий опис
+ field_is_required: Необхідно
+ field_firstname: Ім'я
+ field_lastname: Прізвище
+ field_mail: Ел. пошта
+ field_filename: Файл
+ field_filesize: Розмір
+ field_downloads: Завантаження
+ field_author: Автор
+ field_created_on: Створено
+ field_updated_on: Оновлено
+ field_field_format: Формат
+ field_is_for_all: Для усіх проектів
+ field_possible_values: Можливі значення
+ field_regexp: Регулярний вираз
+ field_min_length: Мінімальна довжина
+ field_max_length: Максимальна довжина
+ field_value: Значення
+ field_category: Категорія
+ field_title: Назва
+ field_project: Проект
+ field_issue: Питання
+ field_status: Статус
+ field_notes: Примітки
+ field_is_closed: Питання закрито
+ field_is_default: Типове значення
+ field_tracker: Координатор
+ field_subject: Тема
+ field_due_date: Дата виконання
+ field_assigned_to: Призначена до
+ field_priority: Пріоритет
+ field_fixed_version: Target version
+ field_user: Користувач
+ field_role: Роль
+ field_homepage: Домашня сторінка
+ field_is_public: Публічний
+ field_parent: Підпроект
+ field_is_in_chlog: Питання, що відображаються в журналі змін
+ 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: Базове відмітне ім'я
+ field_attr_login: Атрибут Реєстрація
+ field_attr_firstname: Атрибут Ім'я
+ field_attr_lastname: Атрибут Прізвище
+ field_attr_mail: Атрибут Email
+ field_onthefly: Створення користувача на льоту
+ field_start_date: Початок
+ field_done_ratio: % зроблено
+ field_auth_source: Режим аутентифікації
+ field_hide_mail: Приховувати мій email
+ 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: Вживається у пошуку
+
+ 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: email адреса для передачі інформації
+ setting_bcc_recipients: Отримувачі сліпої копії (bcc)
+ setting_host_name: Им'я машини
+ setting_text_formatting: Форматування тексту
+ setting_wiki_compression: Стиснення історії Wiki
+ setting_feeds_limit: Обмеження змісту подачі
+ 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_repositories_encodings: Кодування репозиторія
+ setting_emails_footer: Підпис до електронної пошти
+ setting_protocol: Протокол
+
+ label_user: Користувач
+ label_user_plural: Користувачі
+ label_user_new: Новий користувач
+ label_project: Проект
+ label_project_new: Новий проект
+ label_project_plural: Проекти
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Усі проекти
+ label_project_latest: Останні проекти
+ label_issue: Питання
+ label_issue_new: Нові питання
+ label_issue_plural: Питання
+ label_issue_view_all: Проглянути всі питання
+ label_issues_by: "Питання за {{value}}"
+ label_document: Документ
+ label_document_new: Новий документ
+ label_document_plural: Документи
+ label_role: Роль
+ label_role_plural: Ролі
+ label_role_new: Нова роль
+ label_role_and_permissions: Ролі і права доступу
+ label_member: Учасник
+ label_member_new: Новий учасник
+ label_member_plural: Учасники
+ label_tracker: Координатор
+ label_tracker_plural: Координатори
+ label_tracker_new: Новий Координатор
+ label_workflow: Послідовність дій
+ label_issue_status: Статус питання
+ label_issue_status_plural: Статуси питань
+ label_issue_status_new: Новий статус
+ label_issue_category: Категорія питання
+ label_issue_category_plural: Категорії питань
+ label_issue_category_new: Нова категорія
+ label_custom_field: Поле клієнта
+ label_custom_field_plural: Поля клієнта
+ label_custom_field_new: Нове поле клієнта
+ label_enumerations: Довідники
+ label_enumeration_new: Нове значення
+ label_information: Інформація
+ label_information_plural: Інформація
+ label_please_login: Будь ласка, увійдіть
+ label_register: Зареєструватися
+ label_password_lost: Забули пароль
+ label_home: Домашня сторінка
+ label_my_page: Моя сторінка
+ label_my_account: Мій обліковий запис
+ label_my_projects: Мої проекти
+ label_administration: Адміністрування
+ label_login: Увійти
+ label_logout: Вийти
+ label_help: Допомога
+ label_reported_issues: Створені питання
+ label_assigned_to_me_issues: Мої питання
+ label_last_login: Останнє підключення
+ label_registered_on: Зареєстрований(а)
+ label_activity: Активність
+ label_new: Новий
+ label_logged_as: Увійшов як
+ label_environment: Оточення
+ label_authentication: Аутентифікація
+ label_auth_source: Режим аутентифікації
+ label_auth_source_new: Новий режим аутентифікації
+ label_auth_source_plural: Режими аутентифікації
+ label_subproject_plural: Підпроекти
+ label_min_max_length: Мінімальна - максимальна довжина
+ label_list: Список
+ label_date: Дата
+ label_integer: Цілий
+ label_float: З плаваючою крапкою
+ label_boolean: Логічний
+ label_string: Текст
+ label_text: Довгий текст
+ label_attribute: Атрибут
+ label_attribute_plural: атрибути
+ label_download: "{{count}} Завантажено"
+ label_download_plural: "{{count}} Завантажень"
+ label_no_data: Немає даних для відображення
+ label_change_status: Змінити статус
+ label_history: Історія
+ label_attachment: Файл
+ label_attachment_new: Новий файл
+ label_attachment_delete: Видалити файл
+ label_attachment_plural: Файли
+ label_report: Звіт
+ label_report_plural: Звіти
+ label_news: Новини
+ label_news_new: Додати новину
+ label_news_plural: Новини
+ label_news_latest: Останні новини
+ label_news_view_all: Подивитися всі новини
+ label_change_log: Журнал змін
+ label_settings: Налаштування
+ label_overview: Перегляд
+ label_version: Версія
+ label_version_new: Нова версія
+ label_version_plural: Версії
+ label_confirmation: Підтвердження
+ label_export_to: Експортувати в
+ label_read: Читання...
+ label_public_projects: Публічні проекти
+ label_open_issues: відкрите
+ label_open_issues_plural: відкриті
+ label_closed_issues: закрите
+ label_closed_issues_plural: закриті
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Всього
+ label_permissions: Права доступу
+ label_current_status: Поточний статус
+ label_new_statuses_allowed: Дозволені нові статуси
+ label_all: Усі
+ label_none: Нікому
+ label_nobody: Ніхто
+ label_next: Наступний
+ label_previous: Попередній
+ label_used_by: Використовується
+ label_details: Подробиці
+ label_add_note: Додати зауваження
+ label_per_page: На сторінку
+ label_calendar: Календар
+ label_months_from: місяців(ця) з
+ label_gantt: Діаграма Ганта
+ label_internal: Внутрішній
+ label_last_changes: "останні {{count}} змін"
+ label_change_view_all: Проглянути всі зміни
+ label_personalize_page: Персоналізувати цю сторінку
+ label_comment: Коментувати
+ label_comment_plural: Коментарі
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Залишити коментар
+ label_comment_added: Коментар додано
+ label_comment_delete: Видалити коментарі
+ label_query: Запит клієнта
+ label_query_plural: Запити клієнтів
+ label_query_new: Новий запит
+ label_filter_add: Додати фільтр
+ label_filter_plural: Фільтри
+ label_equals: є
+ label_not_equals: немає
+ label_in_less_than: менш ніж
+ label_in_more_than: більш ніж
+ label_in: у
+ label_today: сьогодні
+ label_this_week: цього тижня
+ label_less_than_ago: менш ніж днів(я) назад
+ label_more_than_ago: більш ніж днів(я) назад
+ label_ago: днів(я) назад
+ label_contains: містить
+ label_not_contains: не містить
+ label_day_plural: днів(я)
+ label_repository: Репозиторій
+ label_browse: Проглянути
+ label_modification: "{{count}} зміна"
+ label_modification_plural: "{{count}} змін"
+ label_revision: Версія
+ label_revision_plural: Версій
+ label_added: додано
+ label_modified: змінене
+ label_deleted: видалено
+ label_latest_revision: Остання версія
+ label_latest_revision_plural: Останні версії
+ label_view_revisions: Проглянути версії
+ label_max_size: Максимальний розмір
+ label_sort_highest: У початок
+ label_sort_higher: Вгору
+ label_sort_lower: Вниз
+ label_sort_lowest: У кінець
+ label_roadmap: Оперативний план
+ label_roadmap_due_in: "Строк {{value}}"
+ label_roadmap_overdue: "{{value}} запізнення"
+ label_roadmap_no_issues: Немає питань для даної версії
+ label_search: Пошук
+ label_result_plural: Результати
+ label_all_words: Всі слова
+ label_wiki: Wiki
+ label_wiki_edit: Редагування Wiki
+ label_wiki_edit_plural: Редагування Wiki
+ label_wiki_page: Сторінка Wiki
+ label_wiki_page_plural: Сторінки Wiki
+ label_index_by_title: Індекс за назвою
+ label_index_by_date: Індекс за датою
+ label_current_version: Поточна версія
+ label_preview: Попередній перегляд
+ label_feed_plural: Подання
+ label_changes_details: Подробиці по всіх змінах
+ label_issue_tracking: Координація питань
+ label_spent_time: Витрачений час
+ label_f_hour: "{{value}} година"
+ label_f_hour_plural: "{{value}} годин(и)"
+ label_time_tracking: Облік часу
+ label_change_plural: Зміни
+ label_statistics: Статистика
+ label_commits_per_month: Подань на місяць
+ label_commits_per_author: Подань на користувача
+ label_view_diff: Проглянути відмінності
+ label_diff_inline: підключений
+ label_diff_side_by_side: поряд
+ label_options: Опції
+ label_copy_workflow_from: Скопіювати послідовність дій з
+ label_permissions_report: Звіт про права доступу
+ label_watched_issues: Проглянуті питання
+ label_related_issues: Зв'язані питання
+ label_applied_status: Застосовний статус
+ label_loading: Завантаження...
+ label_relation_new: Новий зв'язок
+ label_relation_delete: Видалити зв'язок
+ label_relates_to: пов'язане з
+ label_duplicates: дублює
+ label_blocks: блокує
+ label_blocked_by: заблоковане
+ label_precedes: передує
+ label_follows: наступний за
+ label_end_to_start: з кінця до початку
+ label_end_to_end: з кінця до кінця
+ label_start_to_start: з початку до початку
+ label_start_to_end: з початку до кінця
+ label_stay_logged_in: Залишатися в системі
+ label_disabled: відключений
+ label_show_completed_versions: Показати завершені версії
+ label_me: мене
+ label_board: Форум
+ label_board_new: Новий форум
+ label_board_plural: Форуми
+ label_topic_plural: Теми
+ label_message_plural: Повідомлення
+ label_message_last: Останнє повідомлення
+ label_message_new: Нове повідомлення
+ label_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: Послати email для перевірки
+ label_feeds_access_key_created_on: "Ключ доступу RSS створений {{value}} назад "
+ label_module_plural: Модулі
+ label_added_time_by: "Доданий {{author}} {{age}} назад"
+ label_updated_time: "Оновлений {{value}} назад"
+ label_jump_to_a_project: Перейти до проекту...
+ label_file_plural: Файли
+ label_changeset_plural: Набори змін
+ label_default_columns: Типові колонки
+ label_no_change_option: (Немає змін)
+ label_bulk_edit_selected_issues: Редагувати всі вибрані питання
+ label_theme: Тема
+ label_default: Типовий
+ label_search_titles_only: Шукати тільки в назвах
+ label_user_mail_option_all: "Для всіх подій у всіх моїх проектах"
+ label_user_mail_option_selected: "Для всіх подій тільки у вибраному проекті..."
+ label_user_mail_option_none: "Тільки для того, що я проглядаю або в чому я беру участь"
+ label_user_mail_no_self_notified: "Не сповіщати про зміни, які я зробив сам"
+ label_registration_activation_by_email: активація облікового запису електронною поштою
+ label_registration_manual_activation: ручна активація облікового запису
+ label_registration_automatic_activation: автоматична активація облыкового
+ label_my_time_report: Мій звіт витраченого часу
+
+ button_login: Вхід
+ button_submit: Відправити
+ button_save: Зберегти
+ button_check_all: Відзначити все
+ button_uncheck_all: Очистити
+ button_delete: Видалити
+ button_create: Створити
+ button_test: Перевірити
+ button_edit: Редагувати
+ button_add: Додати
+ button_change: Змінити
+ button_apply: Застосувати
+ button_clear: Очистити
+ button_lock: Заблокувати
+ button_unlock: Разблокувати
+ button_download: Завантажити
+ button_list: Список
+ button_view: Переглянути
+ button_move: Перемістити
+ button_back: Назад
+ button_cancel: Відмінити
+ button_activate: Активувати
+ button_sort: Сортувати
+ button_log_time: Записати час
+ button_rollback: Відкотити до даної версії
+ button_watch: Дивитися
+ button_unwatch: Не дивитися
+ button_reply: Відповісти
+ button_archive: Архівувати
+ button_unarchive: Розархівувати
+ button_reset: Перезапустити
+ button_rename: Перейменувати
+ button_change_password: Змінити пароль
+ button_copy: Копіювати
+ button_annotate: Анотувати
+
+ status_active: Активний
+ status_registered: Зареєстрований
+ status_locked: Заблокований
+
+ text_select_mail_notifications: Виберіть дії, на які відсилатиметься повідомлення на електронну пошту.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 означає відсутність заборон
+ text_project_destroy_confirmation: Ви наполягаєте на видаленні цього проекту і всієї інформації, що відноситься до нього?
+ text_workflow_edit: Виберіть роль і координатор для редагування послідовності дій
+ text_are_you_sure: Ви впевнені?
+ text_tip_task_begin_day: день початку задачі
+ text_tip_task_end_day: день завершення задачі
+ text_tip_task_begin_end_day: початок задачі і закінчення цього дня
+ text_project_identifier_info: 'Рядкові букви (a-z), допустимі цифри і дефіс.<br />Збережений ідентифікатор не може бути змінений.'
+ text_caracters_maximum: "{{count}} символів(а) максимум."
+ text_caracters_minimum: "Повинно мати якнайменше {{count}} символів(а) у довжину."
+ text_length_between: "Довжина між {{min}} і {{max}} символів."
+ text_tracker_no_workflow: Для цього координатора послідовність дій не визначена
+ text_unallowed_characters: Заборонені символи
+ text_comma_separated: Допустимі декілька значень (розділені комою).
+ text_issues_ref_in_commit_messages: Посилання та зміна питань у повідомленнях до подавань
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{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: "Для невибраних проектів ви отримуватимете повідомлення тільки про те, що проглядаєте або в чому берете участь (наприклад, питання автором яких ви є або які вам призначені)."
+
+ default_role_manager: Менеджер
+ default_role_developper: Розробник
+ default_role_reporter: Репортер
+ звітів default_tracker_bug: Помилка
+ default_tracker_feature: Властивість
+ default_tracker_support: Підтримка
+ default_issue_status_new: Новий
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Вирішено
+ default_issue_status_feedback: Зворотний зв'язок
+ default_issue_status_closed: Зачинено
+ default_issue_status_rejected: Відмовлено
+ default_doc_category_user: Документація користувача
+ default_doc_category_tech: Технічна документація
+ default_priority_low: Низький
+ default_priority_normal: Нормальний
+ default_priority_high: Високий
+ default_priority_urgent: Терміновий
+ default_priority_immediate: Негайний
+ default_activity_design: Проектування
+ default_activity_development: Розробка
+
+ enumeration_issue_priorities: Пріоритети питань
+ enumeration_doc_categories: Категорії документів
+ enumeration_activities: Дії (облік часу)
+ text_status_changed_by_changeset: "Applied in changeset {{value}}."
+ label_display_per_page: "Per page: {{value}}"
+ label_issue_added: Issue added
+ label_issue_updated: Issue updated
+ setting_per_page_options: Objects per page options
+ notice_default_data_loaded: Default configuration successfully loaded.
+ error_scm_not_found: "Entry and/or revision doesn't exist in the repository."
+ label_associated_revisions: Associated revisions
+ label_document_added: Document added
+ label_message_posted: Message added
+ text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+ error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
+ setting_user_format: Users display format
+ label_age: Age
+ label_file_added: File added
+ label_more: More
+ field_default_value: Default value
+ default_tracker_bug: Bug
+ label_scm: SCM
+ label_general: General
+ button_update: Update
+ text_select_project_modules: 'Select modules to enable for this project:'
+ label_change_properties: Change properties
+ text_load_default_configuration: Load the default configuration
+ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+ label_news_added: News added
+ label_repository_plural: Repositories
+ error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
+ project_module_boards: Boards
+ project_module_issue_tracking: Issue tracking
+ project_module_wiki: Wiki
+ project_module_files: Files
+ project_module_documents: Documents
+ project_module_repository: Repository
+ project_module_news: News
+ project_module_time_tracking: Time tracking
+ text_file_repository_writable: File repository writable
+ text_default_administrator_account_changed: Default administrator account changed
+ text_rmagick_available: RMagick available (optional)
+ button_configure: Configure
+ label_plugins: Plugins
+ label_ldap_authentication: LDAP authentication
+ label_downloads_abbr: D/L
+ label_this_month: this month
+ label_last_n_days: "last {{count}} days"
+ label_all_time: all time
+ label_this_year: this year
+ label_date_range: Date range
+ label_last_week: last week
+ label_yesterday: yesterday
+ label_last_month: last month
+ label_add_another_file: Add another file
+ label_optional_description: Optional description
+ text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
+ error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
+ text_assign_time_entries_to_project: Assign reported hours to the project
+ text_destroy_time_entries: Delete reported hours
+ text_reassign_time_entries: 'Reassign reported hours to this issue:'
+ setting_activity_days_default: Days displayed on project activity
+ label_chronological_order: In chronological order
+ field_comments_sorting: Display comments
+ label_reverse_chronological_order: In reverse chronological order
+ label_preferences: Preferences
+ setting_display_subprojects_issues: Display subprojects issues on main projects by default
+ label_overall_activity: Overall activity
+ setting_default_projects_public: New projects are public by default
+ error_scm_annotate: "The entry does not exist or can not be annotated."
+ label_planning: Planning
+ text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
+ label_and_its_subprojects: "{{value}} and its subprojects"
+ mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
+ mail_subject_reminder: "{{count}} issue(s) due in the next days"
+ text_user_wrote: "{{value}} wrote:"
+ label_duplicated_by: duplicated by
+ setting_enabled_scm: Enabled SCM
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ label_incoming_emails: Incoming emails
+ label_generate_key: Generate a key
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: API key
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+ field_parent_title: Parent page
+ label_issue_watchers: Watchers
+ setting_commit_logs_encoding: Commit messages encoding
+ button_quote: Quote
+ setting_sequential_project_identifiers: Generate sequential project identifiers
+ notice_unable_delete_version: Unable to delete version
+ label_renamed: renamed
+ label_copied: copied
+ setting_plain_text_mail: plain text only (no HTML)
+ permission_view_files: View files
+ permission_edit_issues: Edit issues
+ permission_edit_own_time_entries: Edit own time logs
+ permission_manage_public_queries: Manage public queries
+ permission_add_issues: Add issues
+ permission_log_time: Log spent time
+ permission_view_changesets: View changesets
+ permission_view_time_entries: View spent time
+ permission_manage_versions: Manage versions
+ permission_manage_wiki: Manage wiki
+ permission_manage_categories: Manage issue categories
+ permission_protect_wiki_pages: Protect wiki pages
+ permission_comment_news: Comment news
+ permission_delete_messages: Delete messages
+ permission_select_project_modules: Select project modules
+ permission_manage_documents: Manage documents
+ permission_edit_wiki_pages: Edit wiki pages
+ permission_add_issue_watchers: Add watchers
+ permission_view_gantt: View gantt chart
+ permission_move_issues: Move issues
+ permission_manage_issue_relations: Manage issue relations
+ permission_delete_wiki_pages: Delete wiki pages
+ permission_manage_boards: Manage boards
+ permission_delete_wiki_pages_attachments: Delete attachments
+ permission_view_wiki_edits: View wiki history
+ permission_add_messages: Post messages
+ permission_view_messages: View messages
+ permission_manage_files: Manage files
+ permission_edit_issue_notes: Edit notes
+ permission_manage_news: Manage news
+ permission_view_calendar: View calendrier
+ permission_manage_members: Manage members
+ permission_edit_messages: Edit messages
+ permission_delete_issues: Delete issues
+ permission_view_issue_watchers: View watchers list
+ permission_manage_repository: Manage repository
+ permission_commit_access: Commit access
+ permission_browse_repository: Browse repository
+ permission_view_documents: View documents
+ permission_edit_project: Edit project
+ permission_add_issue_notes: Add notes
+ permission_save_queries: Save queries
+ permission_view_wiki_pages: View wiki
+ permission_rename_wiki_pages: Rename wiki pages
+ permission_edit_time_entries: Edit time logs
+ permission_edit_own_issue_notes: Edit own notes
+ setting_gravatar_enabled: Use Gravatar user icons
+ label_example: Example
+ text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
+ permission_edit_own_messages: Edit own messages
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Vietnamese translation for Ruby on Rails
+# by
+# Do Hai Bac (dohaibac@gmail.com)
+# Dao Thanh Ngoc (ngocdaothanh@gmail.com, http://github.com/ngocdaothanh/rails-i18n/tree/master)
+
+vi:
+ number:
+ # Used in number_with_delimiter()
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format:
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ","
+ # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: "."
+ # Number of decimals, behind the separator (1 with a precision of 2 gives: 1.00)
+ precision: 3
+
+ # Used in number_to_currency()
+ currency:
+ format:
+ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
+ format: "%n %u"
+ unit: "đồng"
+ # These three are to override number.format and are optional
+ separator: ","
+ delimiter: "."
+ precision: 2
+
+ # Used in number_to_percentage()
+ percentage:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_precision()
+ precision:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_human_size()
+ human:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ precision: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
+ datetime:
+ distance_in_words:
+ half_a_minute: "30 giây"
+ less_than_x_seconds:
+ one: "chưa tới 1 giây"
+ other: "chưa tới {{count}} giây"
+ x_seconds:
+ one: "1 giây"
+ other: "{{count}} giây"
+ less_than_x_minutes:
+ one: "chưa tới 1 phút"
+ other: "chưa tới {{count}} phút"
+ x_minutes:
+ one: "1 phút"
+ other: "{{count}} phút"
+ about_x_hours:
+ one: "khoảng 1 giờ"
+ other: "khoảng {{count}} giờ"
+ x_days:
+ one: "1 ngày"
+ other: "{{count}} ngày"
+ about_x_months:
+ one: "khoảng 1 tháng"
+ other: "khoảng {{count}} tháng"
+ x_months:
+ one: "1 tháng"
+ other: "{{count}} tháng"
+ about_x_years:
+ one: "khoảng 1 năm"
+ other: "khoảng {{count}} năm"
+ over_x_years:
+ one: "hơn 1 năm"
+ other: "hơn {{count}} năm"
+ prompts:
+ year: "Năm"
+ month: "Tháng"
+ day: "Ngày"
+ hour: "Giờ"
+ minute: "Phút"
+ second: "Giây"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "1 lỗi ngăn không cho lưu {{model}} này"
+ other: "{{count}} lỗi ngăn không cho lưu {{model}} này"
+ # The variable :count is also available
+ body: "Có lỗi với các mục sau:"
+
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "không có trong danh sách"
+ exclusion: "đã được giành trước"
+ invalid: "không hợp lệ"
+ confirmation: "không khớp với xác nhận"
+ accepted: "phải được đồng ý"
+ empty: "không thể rỗng"
+ blank: "không thể để trắng"
+ too_long: "quá dài (tối đa {{count}} ký tự)"
+ too_short: "quá ngắn (tối thiểu {{count}} ký tự)"
+ wrong_length: "độ dài không đúng (phải là {{count}} ký tự)"
+ taken: "đã có"
+ not_a_number: "không phải là số"
+ greater_than: "phải lớn hơn {{count}}"
+ greater_than_or_equal_to: "phải lớn hơn hoặc bằng {{count}}"
+ equal_to: "phải bằng {{count}}"
+ less_than: "phải nhỏ hơn {{count}}"
+ less_than_or_equal_to: "phải nhỏ hơn hoặc bằng {{count}}"
+ odd: "phải là số chẵn"
+ even: "phải là số lẻ"
+ greater_than_start_date: "phải đi sau ngày bắt đầu"
+ not_same_project: "không thuộc cùng dự án"
+ circular_dependency: "quan hệ có thể gây ra lặp vô tận"
+
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%d-%m-%Y"
+ short: "%d %b"
+ long: "%d %B, %Y"
+
+ day_names: ["Chủ nhật", "Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm", "Thứ sáu", "Thứ bảy"]
+ abbr_day_names: ["Chủ nhật", "Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm", "Thứ sáu", "Thứ bảy"]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, "Tháng một", "Tháng hai", "Tháng ba", "Tháng tư", "Tháng năm", "Tháng sáu", "Tháng bảy", "Tháng tám", "Tháng chín", "Tháng mười", "Tháng mười một", "Tháng mười hai"]
+ abbr_month_names: [~, "Tháng một", "Tháng hai", "Tháng ba", "Tháng tư", "Tháng năm", "Tháng sáu", "Tháng bảy", "Tháng tám", "Tháng chín", "Tháng mười", "Tháng mười một", "Tháng mười hai"]
+ # Used in date_select and datime_select.
+ order: [ :day, :month, :year ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ time: "%H:%M"
+ short: "%d %b %H:%M"
+ long: "%d %B, %Y %H:%M"
+ am: "sáng"
+ pm: "chiều"
+
+ # Used in array.to_sentence.
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: " và "
+ last_word_connector: ", và "
+
+ actionview_instancetag_blank_option: Vui lòng chọn
+
+ general_text_No: 'Không'
+ general_text_Yes: 'Có'
+ general_text_no: 'không'
+ general_text_yes: 'có'
+ general_lang_name: 'Tiếng Việt'
+ 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: Cập nhật tài khoản thành công.
+ notice_account_invalid_creditentials: Tài khoản hoặc mật mã không hợp lệ
+ notice_account_password_updated: Cập nhật mật mã thành công.
+ notice_account_wrong_password: Sai mật mã
+ notice_account_register_done: Tài khoản được tạo thành công. Để kích hoạt vui lòng làm theo hướng dẫn trong email gửi đến bạn.
+ notice_account_unknown_email: Không rõ tài khoản.
+ notice_can_t_change_password: Tài khoản được chứng thực từ nguồn bên ngoài. Không thể đổi mật mã cho loại chứng thực này.
+ notice_account_lost_email_sent: Thông tin để đổi mật mã mới đã gửi đến bạn qua email.
+ notice_account_activated: Tài khoản vừa được kích hoạt. Bây giờ bạn có thể đăng nhập.
+ notice_successful_create: Tạo thành công.
+ notice_successful_update: Cập nhật thành công.
+ notice_successful_delete: Xóa thành công.
+ notice_successful_connection: Kết nối thành công.
+ notice_file_not_found: Trang bạn cố xem không tồn tại hoặc đã chuyển.
+ notice_locking_conflict: Thông tin đang được cập nhật bởi người khác. Hãy chép nội dung cập nhật của bạn vào clipboard.
+ notice_not_authorized: Bạn không có quyền xem trang này.
+ notice_email_sent: "Email đã được gửi tới {{value}}"
+ notice_email_error: "Lỗi xảy ra khi gửi email ({{value}})"
+ notice_feeds_access_key_reseted: Mã số chứng thực RSS đã được tạo lại.
+ notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
+ notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
+ notice_account_pending: "Thông tin tài khoản đã được tạo ra và đang chờ chứng thực từ ban quản trị."
+ notice_default_data_loaded: Đã nạp cấu hình mặc định.
+ notice_unable_delete_version: Không thể xóa phiên bản.
+
+ error_can_t_load_default_data: "Không thể nạp cấu hình mặc định: {{value}}"
+ error_scm_not_found: "The entry or revision was not found in the repository."
+ error_scm_command_failed: "Lỗi xảy ra khi truy cập vào kho lưu trữ: {{value}}"
+ error_scm_annotate: "The entry does not exist or can not be annotated."
+ error_issue_not_found_in_project: 'Vấn đề không tồn tại hoặc không thuộc dự án'
+
+ mail_subject_lost_password: "{{value}}: mật mã của bạn"
+ mail_body_lost_password: "Để đổi mật mã, hãy click chuột vào liên kết sau:"
+ mail_subject_register: "{{value}}: kích hoạt tài khoản"
+ mail_body_register: "Để kích hoạt tài khoản, hãy click chuột vào liên kết sau:"
+ mail_body_account_information_external: " Bạn có thể dùng tài khoản {{value}} để đăng nhập."
+ mail_body_account_information: Thông tin về tài khoản
+ mail_subject_account_activation_request: "{{value}}: Yêu cầu chứng thực tài khoản"
+ mail_body_account_activation_request: "Người dùng ({{value}}) mới đăng ký và cần bạn xác nhận:"
+ mail_subject_reminder: "{{count}} vấn đề hết hạn trong các ngày tới"
+ mail_body_reminder: "{{count}} vấn đề gán cho bạn sẽ hết hạn trong {{days}} ngày tới:"
+
+ gui_validation_error: 1 lỗi
+ gui_validation_error_plural: "{{count}} lỗi"
+
+ field_name: Tên
+ field_description: Mô tả
+ field_summary: Tóm tắt
+ field_is_required: Bắt buộc
+ field_firstname: Tên lót + Tên
+ field_lastname: Họ
+ field_mail: Email
+ field_filename: Tập tin
+ field_filesize: Cỡ
+ field_downloads: Tải về
+ field_author: Tác giả
+ field_created_on: Tạo
+ field_updated_on: Cập nhật
+ field_field_format: Định dạng
+ field_is_for_all: Cho mọi dự án
+ field_possible_values: Giá trị hợp lệ
+ field_regexp: Biểu thức chính quy
+ field_min_length: Chiều dài tối thiểu
+ field_max_length: Chiều dài tối đa
+ field_value: Giá trị
+ field_category: Chủ đề
+ field_title: Tiêu đề
+ field_project: Dự án
+ field_issue: Vấn đề
+ field_status: Trạng thái
+ field_notes: Ghi chú
+ field_is_closed: Vấn đề đóng
+ field_is_default: Giá trị mặc định
+ field_tracker: Dòng vấn đề
+ field_subject: Chủ đề
+ field_due_date: Hết hạn
+ field_assigned_to: Gán cho
+ field_priority: Ưu tiên
+ field_fixed_version: Phiên bản
+ field_user: Người dùng
+ field_role: Quyền
+ field_homepage: Trang chủ
+ field_is_public: Công cộng
+ field_parent: Dự án con của
+ field_is_in_chlog: Có thể thấy trong Thay đổi
+ field_is_in_roadmap: Có thể thấy trong Kế hoạch
+ field_login: Đăng nhập
+ field_mail_notification: Thông báo qua email
+ field_admin: Quản trị
+ field_last_login_on: Kết nối cuối
+ field_language: Ngôn ngữ
+ field_effective_date: Ngày
+ field_password: Mật mã
+ field_new_password: Mật mã mới
+ field_password_confirmation: Khẳng định lại
+ field_version: Phiên bản
+ field_type: Kiểu
+ field_host: Host
+ field_port: Port
+ field_account: Tài khoản
+ field_base_dn: Base DN
+ field_attr_login: Login attribute
+ field_attr_firstname: Firstname attribute
+ field_attr_lastname: Lastname attribute
+ field_attr_mail: Email attribute
+ field_onthefly: On-the-fly user creation
+ field_start_date: Bắt đầu
+ field_done_ratio: Tiến độ
+ field_auth_source: Authentication mode
+ field_hide_mail: Không làm lộ email của bạn
+ field_comments: Bình luận
+ field_url: URL
+ field_start_page: Trang bắt đầu
+ field_subproject: Dự án con
+ field_hours: Giờ
+ field_activity: Hoạt động
+ field_spent_on: Ngày
+ field_identifier: Mã nhận dạng
+ field_is_filter: Dùng như một lọc
+ field_issue_to: Vấn đền liên quan
+ field_delay: Độ trễ
+ field_assignable: Vấn đề có thể gán cho vai trò này
+ field_redirect_existing_links: Chuyển hướng trang đã có
+ field_estimated_hours: Thời gian ước đoán
+ field_column_names: Cột
+ field_time_zone: Múi giờ
+ field_searchable: Tìm kiếm được
+ field_default_value: Giá trị mặc định
+ field_comments_sorting: Liệt kê bình luận
+ field_parent_title: Trang mẹ
+
+ setting_app_title: Tựa đề ứng dụng
+ setting_app_subtitle: Tựa đề nhỏ của ứng dụng
+ setting_welcome_text: Thông điệp chào mừng
+ setting_default_language: Ngôn ngữ mặc định
+ setting_login_required: Cần đăng nhập
+ setting_self_registration: Tự chứng thực
+ setting_attachment_max_size: Cỡ tối đa của tập tin đính kèm
+ setting_issues_export_limit: Issues export limit
+ setting_mail_from: Emission email address
+ setting_bcc_recipients: Tạo bản CC bí mật (bcc)
+ setting_host_name: Tên miền và đường dẫn
+ setting_text_formatting: Định dạng bài viết
+ setting_wiki_compression: Wiki history compression
+ setting_feeds_limit: Giới hạn nội dung của feed
+ setting_default_projects_public: Dự án mặc định là công cộng
+ setting_autofetch_changesets: Autofetch commits
+ setting_sys_api_enabled: Enable WS for repository management
+ setting_commit_ref_keywords: Từ khóa tham khảo
+ setting_commit_fix_keywords: Từ khóa chỉ vấn đề đã giải quyết
+ setting_autologin: Tự động đăng nhập
+ setting_date_format: Định dạng ngày
+ setting_time_format: Định dạng giờ
+ setting_cross_project_issue_relations: Cho phép quan hệ chéo giữa các dự án
+ setting_issue_list_default_columns: Default columns displayed on the issue list
+ setting_repositories_encodings: Repositories encodings
+ setting_commit_logs_encoding: Commit messages encoding
+ setting_emails_footer: Chữ ký cuối thư
+ setting_protocol: Giao thức
+ setting_per_page_options: Objects per page options
+ setting_user_format: Định dạng hiển thị người dùng
+ setting_activity_days_default: Days displayed on project activity
+ setting_display_subprojects_issues: Display subprojects issues on main projects by default
+ setting_enabled_scm: Enabled SCM
+ setting_mail_handler_api_enabled: Enable WS for incoming emails
+ setting_mail_handler_api_key: Mã số API
+ setting_sequential_project_identifiers: Tự sinh chuỗi ID dự án
+
+ project_module_issue_tracking: Theo dõi vấn đề
+ project_module_time_tracking: Theo dõi thời gian
+ project_module_news: Tin tức
+ project_module_documents: Tài liệu
+ project_module_files: Tập tin
+ project_module_wiki: Wiki
+ project_module_repository: Kho lưu trữ
+ project_module_boards: Diễn đàn
+
+ label_user: Tài khoản
+ label_user_plural: Tài khoản
+ label_user_new: Tài khoản mới
+ label_project: Dự án
+ label_project_new: Dự án mới
+ label_project_plural: Dự án
+ label_x_projects:
+ zero: no projects
+ one: 1 project
+ other: "{{count}} projects"
+ label_project_all: Mọi dự án
+ label_project_latest: Dự án mới nhất
+ label_issue: Vấn đề
+ label_issue_new: Tạo vấn đề mới
+ label_issue_plural: Vấn đề
+ label_issue_view_all: Tất cả vấn đề
+ label_issues_by: "Vấn đề của {{value}}"
+ label_issue_added: Đã thêm vấn đề
+ label_issue_updated: Vấn đề được cập nhật
+ label_document: Tài liệu
+ label_document_new: Tài liệu mới
+ label_document_plural: Tài liệu
+ label_document_added: Đã thêm tài liệu
+ label_role: Vai trò
+ label_role_plural: Vai trò
+ label_role_new: Vai trò mới
+ label_role_and_permissions: Vai trò và Quyền hạn
+ label_member: Thành viên
+ label_member_new: Thành viên mới
+ label_member_plural: Thành viên
+ label_tracker: Dòng vấn đề
+ label_tracker_plural: Dòng vấn đề
+ label_tracker_new: Tạo dòng vấn đề mới
+ label_workflow: Workflow
+ label_issue_status: Issue status
+ label_issue_status_plural: Issue statuses
+ label_issue_status_new: New status
+ label_issue_category: Chủ đề
+ label_issue_category_plural: Chủ đề
+ label_issue_category_new: Chủ đề mới
+ label_custom_field: Custom field
+ label_custom_field_plural: Custom fields
+ label_custom_field_new: New custom field
+ label_enumerations: Enumerations
+ label_enumeration_new: New value
+ label_information: Thông tin
+ label_information_plural: Thông tin
+ label_please_login: Vui lòng đăng nhập
+ label_register: Đăng ký
+ label_password_lost: Phục hồi mật mã
+ label_home: Trang chính
+ label_my_page: Trang riêng
+ label_my_account: Cá nhân
+ label_my_projects: Dự án của bạn
+ label_administration: Quản trị
+ label_login: Đăng nhập
+ label_logout: Thoát
+ label_help: Giúp đỡ
+ label_reported_issues: Vấn đề đã báo cáo
+ label_assigned_to_me_issues: Vấn đề gán cho bạn
+ label_last_login: Kết nối cuối
+ label_registered_on: Ngày tham gia
+ label_activity: Hoạt động
+ label_overall_activity: Tất cả hoạt động
+ label_new: Mới
+ label_logged_as: Tài khoản »
+ label_environment: Environment
+ label_authentication: Authentication
+ label_auth_source: Authentication mode
+ label_auth_source_new: New authentication mode
+ label_auth_source_plural: Authentication modes
+ label_subproject_plural: Dự án con
+ label_and_its_subprojects: "{{value}} và dự án con"
+ label_min_max_length: Min - Max length
+ label_list: List
+ label_date: Ngày
+ label_integer: Integer
+ label_float: Float
+ label_boolean: Boolean
+ label_string: Text
+ label_text: Long text
+ label_attribute: Attribute
+ label_attribute_plural: Attributes
+ label_download: "{{count}} lần tải"
+ label_download_plural: "{{count}} lần tải"
+ label_no_data: Chưa có thông tin gì
+ label_change_status: Đổi trạng thái
+ label_history: Lược sử
+ label_attachment: Tập tin
+ label_attachment_new: Thêm tập tin mới
+ label_attachment_delete: Xóa tập tin
+ label_attachment_plural: Tập tin
+ label_file_added: Đã thêm tập tin
+ label_report: Báo cáo
+ label_report_plural: Báo cáo
+ label_news: Tin tức
+ label_news_new: Thêm tin
+ label_news_plural: Tin tức
+ label_news_latest: Tin mới
+ label_news_view_all: Xem mọi tin
+ label_news_added: Đã thêm tin
+ label_change_log: Nhật ký thay đổi
+ label_settings: Thiết lập
+ label_overview: Tóm tắt
+ label_version: Phiên bản
+ label_version_new: Phiên bản mới
+ label_version_plural: Phiên bản
+ label_confirmation: Khẳng định
+ label_export_to: 'Định dạng khác của trang này:'
+ label_read: Read...
+ label_public_projects: Các dự án công cộng
+ label_open_issues: mở
+ label_open_issues_plural: mở
+ label_closed_issues: đóng
+ label_closed_issues_plural: đóng
+ label_x_open_issues_abbr_on_total:
+ zero: 0 open / {{total}}
+ one: 1 open / {{total}}
+ other: "{{count}} open / {{total}}"
+ label_x_open_issues_abbr:
+ zero: 0 open
+ one: 1 open
+ other: "{{count}} open"
+ label_x_closed_issues_abbr:
+ zero: 0 closed
+ one: 1 closed
+ other: "{{count}} closed"
+ label_total: Tổng cộng
+ label_permissions: Quyền
+ label_current_status: Trạng thái hiện tại
+ label_new_statuses_allowed: Trạng thái mới được phép
+ label_all: tất cả
+ label_none: không
+ label_nobody: Chẳng ai
+ label_next: Sau
+ label_previous: Trước
+ label_used_by: Used by
+ label_details: Chi tiết
+ label_add_note: Thêm ghi chú
+ label_per_page: Mỗi trang
+ label_calendar: Lịch
+ label_months_from: tháng từ
+ label_gantt: Biểu đồ sự kiện
+ label_internal: Nội bộ
+ label_last_changes: "{{count}} thay đổi cuối"
+ label_change_view_all: Xem mọi thay đổi
+ label_personalize_page: Điều chỉnh trang này
+ label_comment: Bình luận
+ label_comment_plural: Bình luận
+ label_x_comments:
+ zero: no comments
+ one: 1 comment
+ other: "{{count}} comments"
+ label_comment_add: Thêm bình luận
+ label_comment_added: Đã thêm bình luận
+ label_comment_delete: Xóa bình luận
+ label_query: Truy vấn riêng
+ label_query_plural: Truy vấn riêng
+ label_query_new: Truy vấn mới
+ label_filter_add: Thêm lọc
+ label_filter_plural: Bộ lọc
+ label_equals: là
+ label_not_equals: không là
+ label_in_less_than: ít hơn
+ label_in_more_than: nhiều hơn
+ label_in: trong
+ label_today: hôm nay
+ label_all_time: mọi thời gian
+ label_yesterday: hôm qua
+ label_this_week: tuần này
+ label_last_week: tuần trước
+ label_last_n_days: "{{count}} ngày cuối"
+ label_this_month: tháng này
+ label_last_month: tháng cuối
+ label_this_year: năm này
+ label_date_range: Thời gian
+ label_less_than_ago: cách đây dưới
+ label_more_than_ago: cách đây hơn
+ label_ago: cách đây
+ label_contains: chứa
+ label_not_contains: không chứa
+ label_day_plural: ngày
+ label_repository: Kho lưu trữ
+ label_repository_plural: Kho lưu trữ
+ label_browse: Duyệt
+ label_modification: "{{count}} thay đổi"
+ label_modification_plural: "{{count}} thay đổi"
+ label_revision: Bản điều chỉnh
+ label_revision_plural: Bản điều chỉnh
+ label_associated_revisions: Associated revisions
+ label_added: thêm
+ label_modified: đổi
+ label_copied: chép
+ label_renamed: đổi tên
+ label_deleted: xóa
+ label_latest_revision: Bản điều chỉnh cuối cùng
+ label_latest_revision_plural: Bản điều chỉnh cuối cùng
+ label_view_revisions: Xem các bản điều chỉnh
+ label_max_size: Dung lượng tối đa
+ label_sort_highest: Lên trên cùng
+ label_sort_higher: Dịch lên
+ label_sort_lower: Dịch xuống
+ label_sort_lowest: Xuống dưới cùng
+ label_roadmap: Kế hoạch
+ label_roadmap_due_in: "Hết hạn trong {{value}}"
+ label_roadmap_overdue: "Trễ {{value}}"
+ label_roadmap_no_issues: Không có vấn đề cho phiên bản này
+ label_search: Tìm
+ label_result_plural: Kết quả
+ label_all_words: Mọi từ
+ label_wiki: Wiki
+ label_wiki_edit: Wiki edit
+ label_wiki_edit_plural: Thay đổi wiki
+ label_wiki_page: Trang wiki
+ label_wiki_page_plural: Trang wiki
+ label_index_by_title: Danh sách theo tên
+ label_index_by_date: Danh sách theo ngày
+ label_current_version: Bản hiện tại
+ label_preview: Xem trước
+ label_feed_plural: Feeds
+ label_changes_details: Chi tiết của mọi thay đổi
+ label_issue_tracking: Vấn đề
+ label_spent_time: Thời gian
+ label_f_hour: "{{value}} giờ"
+ label_f_hour_plural: "{{value}} giờ"
+ label_time_tracking: Theo dõi thời gian
+ label_change_plural: Thay đổi
+ label_statistics: Thống kê
+ label_commits_per_month: Commits per month
+ label_commits_per_author: Commits per author
+ label_view_diff: So sánh
+ label_diff_inline: inline
+ label_diff_side_by_side: side by side
+ label_options: Tùy chọn
+ label_copy_workflow_from: Copy workflow from
+ label_permissions_report: Thống kê các quyền
+ label_watched_issues: Chủ đề đang theo dõi
+ label_related_issues: Liên quan
+ label_applied_status: Trạng thái áp dụng
+ label_loading: Đang xử lý...
+ label_relation_new: Quan hệ mới
+ label_relation_delete: Xóa quan hệ
+ label_relates_to: liên quan
+ label_duplicates: trùng với
+ label_duplicated_by: bị trùng bởi
+ label_blocks: chặn
+ label_blocked_by: chặn bởi
+ label_precedes: đi trước
+ label_follows: đi sau
+ label_end_to_start: cuối tới đầu
+ label_end_to_end: cuối tới cuối
+ label_start_to_start: đầu tớ đầu
+ label_start_to_end: đầu tới cuối
+ label_stay_logged_in: Lưu thông tin đăng nhập
+ label_disabled: bị vô hiệu
+ label_show_completed_versions: Xem phiên bản đã xong
+ label_me: tôi
+ label_board: Diễn đàn
+ label_board_new: Tạo diễn đàn mới
+ label_board_plural: Diễn đàn
+ label_topic_plural: Chủ đề
+ label_message_plural: Diễn đàn
+ label_message_last: Bài cuối
+ label_message_new: Tạo bài mới
+ label_message_posted: Đã thêm bài viết
+ label_reply_plural: Hồi âm
+ label_send_information: Gửi thông tin đến người dùng qua email
+ label_year: Năm
+ label_month: Tháng
+ label_week: Tuần
+ label_date_from: Từ
+ label_date_to: Đến
+ label_language_based: Theo ngôn ngữ người dùng
+ label_sort_by: "Sắp xếp theo {{value}}"
+ label_send_test_email: Send a test email
+ label_feeds_access_key_created_on: "Mã chứng thực RSS được tạo ra cách đây {{value}}"
+ label_module_plural: Mô-đun
+ label_added_time_by: "thêm bởi {{author}} cách đây {{age}}"
+ label_updated_time: "Cập nhật cách đây {{value}}"
+ label_jump_to_a_project: Nhảy đến dự án...
+ label_file_plural: Tập tin
+ label_changeset_plural: Thay đổi
+ label_default_columns: Cột mặc định
+ label_no_change_option: (không đổi)
+ label_bulk_edit_selected_issues: Sửa nhiều vấn đề
+ label_theme: Giao diện
+ label_default: Mặc định
+ label_search_titles_only: Chỉ tìm trong tựa đề
+ label_user_mail_option_all: "Mọi sự kiện trên mọi dự án của bạn"
+ label_user_mail_option_selected: "Mọi sự kiện trên các dự án được chọn..."
+ label_user_mail_option_none: "Chỉ những vấn đề bạn theo dõi hoặc được gán"
+ label_user_mail_no_self_notified: "Đừng gửi email về các thay đổi do chính bạn thực hiện"
+ label_registration_activation_by_email: account activation by email
+ label_registration_manual_activation: manual account activation
+ label_registration_automatic_activation: automatic account activation
+ label_display_per_page: "mỗi trang: {{value}}"
+ label_age: Age
+ label_change_properties: Thay đổi thuộc tính
+ label_general: Tổng quan
+ label_more: Chi tiết
+ label_scm: SCM
+ label_plugins: Mô-đun
+ label_ldap_authentication: Chứng thực LDAP
+ label_downloads_abbr: Tải về
+ label_optional_description: Mô tả bổ sung
+ label_add_another_file: Thêm tập tin khác
+ label_preferences: Cấu hình
+ label_chronological_order: Bài cũ xếp trước
+ label_reverse_chronological_order: Bài mới xếp trước
+ label_planning: Kế hoạch
+ label_incoming_emails: Nhận mail
+ label_generate_key: Tạo mã
+ label_issue_watchers: Theo dõi
+
+ button_login: Đăng nhập
+ button_submit: Gửi
+ button_save: Lưu
+ button_check_all: Đánh dấu tất cả
+ button_uncheck_all: Bỏ dấu tất cả
+ button_delete: Xóa
+ button_create: Tạo
+ button_test: Kiểm tra
+ button_edit: Sửa
+ button_add: Thêm
+ button_change: Đổi
+ button_apply: Áp dụng
+ button_clear: Xóa
+ button_lock: Khóa
+ button_unlock: Mở khóa
+ button_download: Tải về
+ button_list: Liệt kê
+ button_view: Xem
+ button_move: Chuyển
+ button_back: Quay lại
+ button_cancel: Bỏ qua
+ button_activate: Kích hoạt
+ button_sort: Sắp xếp
+ button_log_time: Thêm thời gian
+ button_rollback: Quay trở lại phiên bản này
+ button_watch: Theo dõi
+ button_unwatch: Bỏ theo dõi
+ button_reply: Trả lời
+ button_archive: Đóng băng
+ button_unarchive: Xả băng
+ button_reset: Tạo lại
+ button_rename: Đổi tên
+ button_change_password: Đổi mật mã
+ button_copy: Chép
+ button_annotate: Chú giải
+ button_update: Cập nhật
+ button_configure: Cấu hình
+ button_quote: Trích dẫn
+
+ status_active: hoạt động
+ status_registered: đăng ký
+ status_locked: khóa
+
+ text_select_mail_notifications: Chọn hành động đối với mỗi email thông báo sẽ gửi.
+ text_regexp_info: eg. ^[A-Z0-9]+$
+ text_min_max_length_info: 0 để chỉ không hạn chế
+ text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
+ text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
+ text_workflow_edit: Select a role and a tracker to edit the workflow
+ text_are_you_sure: Bạn chắc chứ?
+ text_tip_task_begin_day: ngày bắt đầu
+ text_tip_task_end_day: ngày kết thúc
+ text_tip_task_begin_end_day: bắt đầu và kết thúc cùng ngày
+ text_project_identifier_info: 'Chỉ cho phép chữ cái thường (a-z), con số và dấu gạch ngang.<br />Sau khi lưu, chỉ số ID không thể thay đổi.'
+ text_caracters_maximum: "Tối đa {{count}} ký tự."
+ text_caracters_minimum: "Phải gồm ít nhất {{count}} ký tự."
+ text_length_between: "Length between {{min}} and {{max}} characters."
+ text_tracker_no_workflow: No workflow defined for this tracker
+ text_unallowed_characters: Ký tự không hợp lệ
+ text_comma_separated: Multiple values allowed (comma separated).
+ text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+ text_issue_added: "Issue {{id}} has been reported by {{author}}."
+ text_issue_updated: "Issue {{id}} has been updated by {{author}}."
+ text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
+ text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
+ text_issue_category_destroy_assignments: Remove category assignments
+ text_issue_category_reassign_to: Reassign issues to this category
+ text_user_mail_option: "Với các dự án không được chọn, bạn chỉ có thể nhận được thông báo về các vấn đề bạn đăng ký theo dõi hoặc có liên quan đến bạn (chẳng hạn, vấn đề được gán cho bạn)."
+ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+ text_load_default_configuration: Load the default configuration
+ text_status_changed_by_changeset: "Applied in changeset {{value}}."
+ text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+ text_select_project_modules: 'Chọn các mô-đun cho dự án:'
+ text_default_administrator_account_changed: Default administrator account changed
+ text_file_repository_writable: File repository writable
+ text_rmagick_available: RMagick available (optional)
+ text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
+ text_destroy_time_entries: Delete reported hours
+ text_assign_time_entries_to_project: Assign reported hours to the project
+ text_reassign_time_entries: 'Reassign reported hours to this issue:'
+ text_user_wrote: "{{value}} wrote:"
+ text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
+ text_enumeration_category_reassign_to: 'Reassign them to this value:'
+ text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+
+ default_role_manager: Điều hành
+ default_role_developper: Phát triển
+ default_role_reporter: Báo cáo
+ default_tracker_bug: Lỗi
+ default_tracker_feature: Tính năng
+ default_tracker_support: Hỗ trợ
+ default_issue_status_new: Mới
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: Quyết tâm
+ default_issue_status_feedback: Phản hồi
+ default_issue_status_closed: Đóng
+ default_issue_status_rejected: Từ chối
+ default_doc_category_user: Tài liệu người dùng
+ default_doc_category_tech: Tài liệu kỹ thuật
+ default_priority_low: Thấp
+ default_priority_normal: Bình thường
+ default_priority_high: Cao
+ default_priority_urgent: Khẩn cấp
+ default_priority_immediate: Trung bình
+ default_activity_design: Thiết kế
+ default_activity_development: Phát triển
+
+ enumeration_issue_priorities: Mức độ ưu tiên vấn đề
+ enumeration_doc_categories: Chủ đề tài liệu
+ enumeration_activities: Hoạt động (theo dõi thời gian)
+
+ setting_plain_text_mail: mail dạng text đơn giản (không dùng HTML)
+ setting_gravatar_enabled: Dùng biểu tượng Gravatar
+ permission_edit_project: Chỉnh dự án
+ permission_select_project_modules: Chọn mô-đun
+ permission_manage_members: Quản lý thành viên
+ permission_manage_versions: Quản lý phiên bản
+ permission_manage_categories: Quản lý chủ đề
+ permission_add_issues: Thêm vấn đề
+ permission_edit_issues: Sửa vấn đề
+ permission_manage_issue_relations: Quản lý quan hệ vấn đề
+ permission_add_issue_notes: Thêm chú thích
+ permission_edit_issue_notes: Sửa chú thích
+ permission_edit_own_issue_notes: Sửa chú thích cá nhân
+ permission_move_issues: Chuyển vấn đề
+ permission_delete_issues: Xóa vấn đề
+ permission_manage_public_queries: Quản lý truy cấn công cộng
+ permission_save_queries: Lưu truy vấn
+ permission_view_gantt: Xem biểu đồ sự kiện
+ permission_view_calendar: Xem lịch
+ permission_view_issue_watchers: Xem các người theo dõi
+ permission_add_issue_watchers: Thêm người theo dõi
+ permission_log_time: Lưu thời gian đã tốn
+ permission_view_time_entries: Xem thời gian đã tốn
+ permission_edit_time_entries: Xem nhật ký thời gian
+ permission_edit_own_time_entries: Sửa thời gian đã lưu
+ permission_manage_news: Quản lý tin mới
+ permission_comment_news: Chú thích vào tin mới
+ permission_manage_documents: Quản lý tài liệu
+ permission_view_documents: Xem tài liệu
+ permission_manage_files: Quản lý tập tin
+ permission_view_files: Xem tập tin
+ permission_manage_wiki: Quản lý wiki
+ permission_rename_wiki_pages: Đổi tên trang wiki
+ permission_delete_wiki_pages: Xóa trang wiki
+ permission_view_wiki_pages: Xem wiki
+ permission_view_wiki_edits: Xem lược sử trang wiki
+ permission_edit_wiki_pages: Sửa trang wiki
+ permission_delete_wiki_pages_attachments: Xóa tệp đính kèm
+ permission_protect_wiki_pages: Bảo vệ trang wiki
+ permission_manage_repository: Quản lý kho lưu trữ
+ permission_browse_repository: Duyệt kho lưu trữ
+ permission_view_changesets: Xem các thay đổi
+ permission_commit_access: Truy cập commit
+ permission_manage_boards: Quản lý diễn đàn
+ permission_view_messages: Xem bài viết
+ permission_add_messages: Gửi bài viết
+ permission_edit_messages: Sửa bài viết
+ permission_edit_own_messages: Sửa bài viết cá nhân
+ permission_delete_messages: Xóa bài viết
+ permission_delete_own_messages: Xóa bài viết cá nhân
+ label_example: Ví dụ
+ text_repository_usernames_mapping: "Chọn hoặc cập nhật ánh xạ người dùng hệ thống với người dùng trong kho lưu trữ.\nNhững trường hợp trùng hợp về tên và email sẽ được tự động ánh xạ."
+ permission_delete_own_messages: Delete own messages
+ label_user_activity: "{{value}}'s activity"
+ label_updated_time_by: "Updated by {{author}} {{age}} ago"
+ text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
+ setting_diff_max_lines_displayed: Max number of diff lines displayed
+ text_plugin_assets_writable: Plugin assets directory writable
+ warning_attachments_not_saved: "{{count}} file(s) could not be saved."
+ button_create_and_continue: Create and continue
+ text_custom_field_possible_values_info: 'One line for each value'
+ label_display: Display
+ field_editable: Editable
+ setting_repository_log_display_limit: Maximum number of revisions displayed on file log
+ setting_file_max_size_displayed: Max size of text files displayed inline
+ field_watcher: Watcher
+ setting_openid: Allow OpenID login and registration
+ field_identity_url: OpenID URL
+ label_login_with_open_id_option: or login with OpenID
+ field_content: Content
+ label_descending: Descending
+ label_sort: Sort
+ label_ascending: Ascending
+ label_date_from_to: From {{start}} to {{end}}
+ label_greater_or_equal: ">="
+ label_less_or_equal: <=
+ text_wiki_page_destroy_question: This page has {{descendants}} child page(s) and descendant(s). What do you want to do?
+ text_wiki_page_reassign_children: Reassign child pages to this parent page
+ text_wiki_page_nullify_children: Keep child pages as root pages
+ text_wiki_page_destroy_children: Delete child pages and all their descendants
+ setting_password_min_length: Minimum password length
+ field_group_by: Group results by
+ mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
+ label_wiki_content_added: Wiki page added
+ mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
+ mail_body_wiki_content_added: The '{{page}}' wiki page has been added by {{author}}.
+ label_wiki_content_updated: Wiki page updated
+ mail_body_wiki_content_updated: The '{{page}}' 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
--- /dev/null
+# Chinese (Taiwan) translations for Ruby on Rails
+# by tsechingho (http://github.com/tsechingho)
+# See http://github.com/svenfuchs/rails-i18n/ for details.
+
+"zh-TW":
+ 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月]
+ # 使用於 date_select 與 datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%Y年%b%d日 %A %H:%M:%S %Z"
+ time: "%H:%M"
+ short: "%b%d日 %H:%M"
+ long: "%Y年%b%d日 %H:%M"
+ am: "AM"
+ pm: "PM"
+
+# 使用於 array.to_sentence.
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: " 和 "
+ last_word_connector: ", 和 "
+ sentence_connector: "且"
+ skip_last_comma: false
+
+ number:
+ # 使用於 number_with_delimiter()
+ # 同時也是 'currency', 'percentage', 'precision', 與 'human' 的預設值
+ format:
+ # 設定小數點分隔字元,以使用更高的準確度 (例如: 1.0 / 2.0 == 0.5)
+ separator: "."
+ # 千分位符號 (例如:一百萬是 1,000,000) (均以三個位數來分組)
+ delimiter: ","
+ # 小數點分隔字元後之精確位數 (數字 1 搭配 2 位精確位數為: 1.00)
+ precision: 3
+
+ # 使用於 number_to_currency()
+ currency:
+ format:
+ # 貨幣符號的位置? %u 是貨幣符號, %n 是數值 (預設值: $5.00)
+ format: "%u%n"
+ unit: "NT$"
+ # 下列三個選項設定, 若有設定值將會取代 number.format 成為預設值
+ separator: "."
+ delimiter: ","
+ precision: 2
+
+ # 使用於 number_to_percentage()
+ percentage:
+ format:
+ # 下列三個選項設定, 若有設定值將會取代 number.format 成為預設值
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # 使用於 number_to_precision()
+ precision:
+ format:
+ # 下列三個選項設定, 若有設定值將會取代 number.format 成為預設值
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # 使用於 number_to_human_size()
+ human:
+ format:
+ # 下列三個選項設定, 若有設定值將會取代 number.format 成為預設值
+ # separator:
+ delimiter: ""
+ precision: 1
+ # 儲存單位輸出格式.
+ # %u 是儲存單位, %n 是數值 (預設值: 2 MB)
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "位元組 (B)"
+ other: "位元組 (B)"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ # 使用於 distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
+ 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_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}} 年"
+ prompts:
+ year: "年"
+ month: "月"
+ day: "日"
+ hour: "時"
+ minute: "分"
+ second: "秒"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "有 1 個錯誤發生使得「{{model}}」無法被儲存。"
+ other: "有 {{count}} 個錯誤發生使得「{{model}}」無法被儲存。"
+ # The variable :count is also available
+ body: "下面所列欄位有問題:"
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "沒有包含在列表中"
+ exclusion: "是被保留的"
+ invalid: "是無效的"
+ confirmation: "不符合確認值"
+ accepted: "必须是可被接受的"
+ empty: "不能留空"
+ blank: "不能是空白字元"
+ too_long: "過長(最長是 {{count}} 個字)"
+ too_short: "過短(最短是 {{count}} 個字)"
+ wrong_length: "字數錯誤(必須是 {{count}} 個字)"
+ taken: "已經被使用"
+ not_a_number: "不是數字"
+ greater_than: "必須大於 {{count}}"
+ greater_than_or_equal_to: "必須大於或等於 {{count}}"
+ equal_to: "必須等於 {{count}}"
+ less_than: "必須小於 {{count}}"
+ less_than_or_equal_to: "必須小於或等於 {{count}}"
+ odd: "必須是奇數"
+ even: "必須是偶數"
+ # Append your own errors here or at the model/attributes scope.
+ greater_than_start_date: "必須在起始日期之後"
+ not_same_project: "不屬於同一個專案"
+ circular_dependency: "這個關聯會導致環狀相依"
+
+ # You can define own errors for models or model attributes.
+ # The values :model, :attribute and :value are always available for interpolation.
+ #
+ # For example,
+ # models:
+ # user:
+ # blank: "This is a custom blank message for {{model}}: {{attribute}}"
+ # attributes:
+ # login:
+ # blank: "This is a custom blank message for User login"
+ # Will define custom blank validation message for User model and
+ # custom blank validation message for login attribute of User model.
+ #models:
+
+ # Translate model names. Used in Model.human_name().
+ #models:
+ # For example,
+ # user: "Dude"
+ # will translate User model name to "Dude"
+
+ # Translate model attribute names. Used in Model.human_attribute_name(attribute).
+ #attributes:
+ # For example,
+ # user:
+ # login: "Handle"
+ # will translate User attribute "login" as "Handle"
+
+ actionview_instancetag_blank_option: 請選擇
+
+ general_text_No: '否'
+ general_text_Yes: '是'
+ general_text_no: '否'
+ general_text_yes: '是'
+ general_lang_name: 'Traditional Chinese (繁體中文)'
+ general_csv_separator: ','
+ general_csv_decimal_separator: '.'
+ general_csv_encoding: Big5
+ general_pdf_encoding: Big5
+ general_first_day_of_week: '7'
+
+ notice_account_updated: 帳戶更新資訊已儲存
+ notice_account_invalid_creditentials: 帳戶或密碼不正確
+ notice_account_password_updated: 帳戶新密碼已儲存
+ notice_account_wrong_password: 密碼不正確
+ notice_account_register_done: 帳號已建立成功。欲啟用您的帳號,請點擊系統確認信函中的啟用連結。
+ notice_account_unknown_email: 未知的使用者
+ notice_can_t_change_password: 這個帳號使用外部認證方式,無法變更其密碼。
+ notice_account_lost_email_sent: 包含選擇新密碼指示的電子郵件,已經寄出給您。
+ notice_account_activated: 您的帳號已經啟用,可用它登入系統。
+ notice_successful_create: 建立成功
+ notice_successful_update: 更新成功
+ notice_successful_delete: 刪除成功
+ notice_successful_connection: 連線成功
+ notice_file_not_found: 您想要存取的頁面已經不存在或被搬移至其他位置。
+ notice_locking_conflict: 資料已被其他使用者更新。
+ notice_not_authorized: 你未被授權存取此頁面。
+ notice_email_sent: "郵件已經成功寄送至以下收件者: {{value}}"
+ notice_email_error: "寄送郵件的過程中發生錯誤 ({{value}})"
+ notice_feeds_access_key_reseted: 您的 RSS 存取鍵已被重新設定。
+ notice_failed_to_save_issues: " {{count}} 個項目儲存失敗 (總共選取 {{total}} 個項目): {{ids}}."
+ notice_no_issue_selected: "未選擇任何項目!請勾選您想要編輯的項目。"
+ notice_account_pending: "您的帳號已經建立,正在等待管理員的審核。"
+ notice_default_data_loaded: 預設組態已載入成功。
+ notice_unable_delete_version: 無法刪除版本。
+
+ error_can_t_load_default_data: "無法載入預設組態: {{value}}"
+ error_scm_not_found: SCM 儲存庫中找不到這個專案或版本。
+ error_scm_command_failed: "嘗試存取儲存庫時發生錯誤: {{value}}"
+ error_scm_annotate: "SCM 儲存庫中無此項目或此項目無法被加註。"
+ error_issue_not_found_in_project: '該項目不存在或不屬於此專案'
+ error_no_tracker_in_project: '此專案尚未指定追蹤標籤。請檢查專案的設定資訊。'
+ error_no_default_issue_status: '尚未定義項目狀態的預設值。請您前往「網站管理」->「項目狀態清單」頁面,檢查相關組態設定。'
+ error_can_not_reopen_issue_on_closed_version: '指派給「已結束」版本的項目,無法再將其狀態變更為「進行中」'
+
+ warning_attachments_not_saved: "{{count}} 個附加檔案無法儲存。"
+
+ mail_subject_lost_password: 您的 Redmine 網站密碼
+ mail_body_lost_password: '欲變更您的 Redmine 網站密碼, 請點選以下鏈結:'
+ mail_subject_register: 啟用您的 Redmine 帳號
+ mail_body_register: '欲啟用您的 Redmine 帳號, 請點選以下鏈結:'
+ mail_body_account_information_external: "您可以使用 {{value}} 帳號登入 Redmine 網站。"
+ mail_body_account_information: 您的 Redmine 帳號資訊
+ mail_subject_account_activation_request: Redmine 帳號啟用需求通知
+ mail_body_account_activation_request: "有位新用戶 ({{value}}) 已經完成註冊,正等候您的審核:"
+ mail_subject_reminder: "您有 {{count}} 個項目即將到期"
+ mail_body_reminder: "{{count}} 個指派給您的項目,將於 {{days}} 天之內到期:"
+ mail_subject_wiki_content_added: "'{{page}}' wiki 頁面已被新增"
+ mail_body_wiki_content_added: "The '{{page}}' wiki 頁面已被 {{author}} 新增。"
+ mail_subject_wiki_content_updated: "'{{page}}' wiki 頁面已被更新"
+ mail_body_wiki_content_updated: "The '{{page}}' wiki 頁面已被 {{author}} 更新。"
+
+ gui_validation_error: 1 個錯誤
+ gui_validation_error_plural: "{{count}} 個錯誤"
+
+ field_name: 名稱
+ field_description: 概述
+ field_summary: 摘要
+ field_is_required: 必填
+ field_firstname: 名字
+ field_lastname: 姓氏
+ field_mail: 電子郵件
+ field_filename: 檔案名稱
+ field_filesize: 大小
+ field_downloads: 下載次數
+ field_author: 作者
+ field_created_on: 建立日期
+ field_updated_on: 更新
+ field_field_format: 格式
+ field_is_for_all: 給全部的專案
+ field_possible_values: 可能值
+ field_regexp: 正規表示式
+ field_min_length: 最小長度
+ field_max_length: 最大長度
+ field_value: 值
+ field_category: 分類
+ field_title: 標題
+ field_project: 專案
+ field_issue: 項目
+ field_status: 狀態
+ field_notes: 筆記
+ field_is_closed: 項目結束
+ field_is_default: 預設值
+ field_tracker: 追蹤標籤
+ field_subject: 主旨
+ field_due_date: 完成日期
+ field_assigned_to: 分派給
+ field_priority: 優先權
+ field_fixed_version: 版本
+ field_user: 用戶
+ field_role: 角色
+ field_homepage: 網站首頁
+ field_is_public: 公開
+ field_parent: 父專案
+ field_is_in_chlog: 項目顯示於變更記錄中
+ 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: Type
+ field_host: 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: 網址
+ 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 網址
+ field_content: 內容
+ field_group_by: 結果分組方式
+
+ setting_app_title: 標題
+ setting_app_subtitle: 副標題
+ setting_welcome_text: 歡迎詞
+ setting_default_language: 預設語系
+ setting_login_required: 需要驗證
+ setting_self_registration: 註冊選項
+ setting_attachment_max_size: 附件大小限制
+ setting_issues_export_limit: 項目匯出限制
+ setting_mail_from: 寄件者電子郵件
+ setting_bcc_recipients: 使用密件副本 (BCC)
+ setting_plain_text_mail: 純文字郵件 (不含 HTML)
+ setting_host_name: 主機名稱
+ setting_text_formatting: 文字格式
+ setting_wiki_compression: 壓縮 Wiki 歷史文章
+ setting_feeds_limit: RSS 新聞限制
+ setting_autofetch_changesets: 自動取得送交版次
+ setting_default_projects_public: 新建立之專案預設為「公開」
+ 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_repositories_encodings: 版本庫編碼
+ setting_commit_logs_encoding: 送交訊息編碼
+ 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_api_enabled: 啟用處理傳入電子郵件的服務
+ setting_mail_handler_api_key: API 金鑰
+ setting_sequential_project_identifiers: 循序產生專案識別碼
+ setting_gravatar_enabled: 啟用 Gravatar 全球認證大頭像
+ setting_gravatar_default: 預設全球認證大頭像圖片
+ 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: 新專案預設啟用的模組
+
+ permission_add_project: 建立專案
+ permission_edit_project: 編輯專案
+ permission_select_project_modules: 選擇專案模組
+ permission_manage_members: 管理成員
+ permission_manage_versions: 管理版本
+ permission_manage_categories: 管理項目分類
+ permission_add_issues: 新增項目
+ permission_edit_issues: 編輯項目
+ permission_manage_issue_relations: 管理項目關聯
+ permission_add_issue_notes: 新增筆記
+ permission_edit_issue_notes: 編輯筆記
+ permission_edit_own_issue_notes: 編輯自己的筆記
+ permission_move_issues: 搬移項目
+ permission_delete_issues: 刪除項目
+ permission_manage_public_queries: 管理公開查詢
+ permission_save_queries: 儲存查詢
+ permission_view_gantt: 檢視甘特圖
+ 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_manage_documents: 管理文件
+ 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: 刪除自己的訊息
+
+ 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: 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_administration: 網站管理
+ label_login: 登入
+ label_logout: 登出
+ label_help: 說明
+ label_reported_issues: 我通報的項目
+ label_assigned_to_me_issues: 分派給我的項目
+ label_last_login: 最近一次連線
+ label_registered_on: 註冊於
+ label_activity: 活動
+ label_overall_activity: 檢視整體活動
+ label_user_activity: "{{value}} 的活動"
+ label_new: 建立新的...
+ label_logged_as: 目前登入
+ label_environment: 環境
+ label_authentication: 認證
+ label_auth_source: 認證模式
+ label_auth_source_new: 建立新認證模式
+ label_auth_source_plural: 認證模式清單
+ label_subproject_plural: 子專案
+ label_and_its_subprojects: "{{value}} 與其子專案"
+ label_min_max_length: 最小 - 最大 長度
+ label_list: 清單
+ label_date: 日期
+ label_integer: 整數
+ label_float: 浮點數
+ label_boolean: 布林
+ label_string: 文字
+ label_text: 長文字
+ label_attribute: 屬性
+ label_attribute_plural: 屬性
+ label_download: "{{count}} 個下載"
+ label_download_plural: "{{count}} 個下載"
+ label_no_data: 沒有任何資料可供顯示
+ label_change_status: 變更狀態
+ label_history: 歷史
+ label_attachment: 檔案
+ label_attachment_new: 建立新檔案
+ label_attachment_delete: 刪除檔案
+ label_attachment_plural: 檔案
+ label_file_added: 檔案已新增
+ label_report: 報告
+ label_report_plural: 報告
+ label_news: 新聞
+ label_news_new: 建立新聞
+ label_news_plural: 新聞
+ label_news_latest: 最近新聞
+ label_news_view_all: 檢視全部的新聞
+ label_news_added: 新聞已新增
+ label_change_log: 變更記錄
+ label_settings: 設定
+ label_overview: 概觀
+ label_version: 版本
+ label_version_new: 建立新版本
+ label_version_plural: 版本
+ label_confirmation: 確認
+ label_export_to: 匯出至
+ label_read: 讀取...
+ label_public_projects: 公開專案
+ label_open_issues: 進行中
+ label_open_issues_plural: 進行中
+ label_closed_issues: 已結束
+ label_closed_issues_plural: 已結束
+ label_x_open_issues_abbr_on_total:
+ zero: 0 進行中 / 共 {{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: 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_modification: "{{count}} 變更"
+ label_modification_plural: "{{count}} 變更"
+ label_branch: 分支
+ label_tag: 標籤
+ label_revision: 版次
+ label_revision_plural: 版次清單
+ label_associated_revisions: 相關版次
+ label_added: 已新增
+ label_modified: 已修改
+ label_copied: 已複製
+ label_renamed: 已重新命名
+ label_deleted: 已刪除
+ label_latest_revision: 最新版次
+ label_latest_revision_plural: 最近版次清單
+ label_view_revisions: 檢視版次清單
+ label_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_f_hour: "{{value}} 小時"
+ label_f_hour_plural: "{{value}} 小時"
+ label_time_tracking: 工時追蹤
+ label_change_plural: 變更
+ label_statistics: 統計資訊
+ label_commits_per_month: 依月份統計送交次數
+ label_commits_per_author: 依作者統計送交次數
+ label_view_diff: 檢視差異
+ label_diff_inline: 直列
+ label_diff_side_by_side: 並排
+ label_options: 選項清單
+ label_copy_workflow_from: 從以下追蹤標籤複製工作流程
+ label_permissions_report: 權限報表
+ label_watched_issues: 觀察中的項目清單
+ label_related_issues: 相關的項目清單
+ label_applied_status: 已套用狀態
+ label_loading: 載入中...
+ label_relation_new: 建立新關聯
+ label_relation_delete: 刪除關聯
+ label_relates_to: 關聯至
+ label_duplicates: 已重複
+ label_duplicated_by: 與後面所列項目重複
+ label_blocks: 阻擋
+ label_blocked_by: 被阻擋
+ label_precedes: 優先於
+ label_follows: 跟隨於
+ label_end_to_start: 結束─開始
+ label_end_to_end: 結束─結束
+ label_start_to_start: 開始─開始
+ label_start_to_end: 開始─結束
+ label_stay_logged_in: 維持已登入狀態
+ label_disabled: 關閉
+ label_show_completed_versions: 顯示已完成的版本
+ label_me: 我自己
+ label_board: 論壇
+ label_board_new: 建立新論壇
+ label_board_plural: 論壇
+ label_topic_plural: 討論主題
+ label_message_plural: 訊息
+ label_message_last: 上一封訊息
+ label_message_new: 建立新訊息
+ label_message_posted: 訊息已新增
+ label_reply_plural: 回應
+ label_send_information: 寄送帳戶資訊電子郵件給用戶
+ label_year: 年
+ label_month: 月
+ label_week: 週
+ label_date_from: 開始
+ label_date_to: 結束
+ label_language_based: 依用戶之語系決定
+ label_sort_by: "按 {{value}} 排序"
+ label_send_test_email: 寄送測試郵件
+ label_feeds_access_key_created_on: "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_no_self_notified: "不提醒我自己所做的變更"
+ label_registration_activation_by_email: 透過電子郵件啟用帳戶
+ label_registration_manual_activation: 手動啟用帳戶
+ label_registration_automatic_activation: 自動啟用帳戶
+ label_display_per_page: "每頁顯示: {{value}} 個"
+ label_age: 年齡
+ label_change_properties: 變更屬性
+ label_general: 一般
+ label_more: 更多 »
+ label_scm: 版本控管
+ label_plugins: 附加元件
+ label_ldap_authentication: LDAP 認證
+ label_downloads_abbr: 下載
+ 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: 耗用工時
+
+ 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_annotate: 註解
+ button_update: 更新
+ button_configure: 設定
+ button_quote: 引用
+
+ status_active: 活動中
+ status_registered: 註冊完成
+ status_locked: 鎖定中
+
+ version_status_open: 進行中
+ version_status_locked: 已鎖定
+ version_status_closed: 已結束
+
+ field_active: 活動中
+
+ text_select_mail_notifications: 選擇欲寄送提醒通知郵件之動作
+ text_regexp_info: eg. ^[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_project_identifier_info: '只允許小寫英文字母(a-z)、阿拉伯數字與連字符號(-)。<br />儲存後,代碼不可再被更改。'
+ text_caracters_maximum: "最多 {{count}} 個字元."
+ text_caracters_minimum: "長度必須大於 {{count}} 個字元."
+ text_length_between: "長度必須介於 {{min}} 至 {{max}} 個字元之間."
+ text_tracker_no_workflow: 此追蹤標籤尚未定義工作流程
+ text_unallowed_characters: 不允許的字元
+ text_comma_separated: 可輸入多個值 (以逗號分隔).
+ text_issues_ref_in_commit_messages: 送交訊息中參照(或修正)項目之關鍵字
+ text_issue_added: "項目 {{id}} 已被 {{author}} 通報。"
+ text_issue_updated: "項目 {{id}} 已被 {{author}} 更新。"
+ text_wiki_destroy_confirmation: 您確定要刪除這個 wiki 和其中的所有內容?
+ text_issue_category_destroy_question: "有 ({{count}}) 個項目被指派到此分類. 請選擇您想要的動作?"
+ text_issue_category_destroy_assignments: 移除這些項目的分類
+ text_issue_category_reassign_to: 重新指派這些項目至其它分類
+ text_user_mail_option: "對於那些未被選擇的專案,將只會接收到您正在觀察中,或是參與中的項目通知。(「參與中的項目」包含您建立的或是指派給您的項目)"
+ text_no_configuration_data: "角色、追蹤器、項目狀態與流程尚未被設定完成。\n強烈建議您先載入預設的設定,然後修改成您想要的設定。"
+ text_load_default_configuration: 載入預設組態
+ text_status_changed_by_changeset: "已套用至變更集 {{value}}."
+ text_issues_destroy_confirmation: '確定刪除已選擇的項目?'
+ text_select_project_modules: '選擇此專案可使用之模組:'
+ text_default_administrator_account_changed: 已變更預設管理員帳號內容
+ text_file_repository_writable: 可寫入附加檔案目錄
+ text_plugin_assets_writable: 可寫入附加元件目錄
+ text_rmagick_available: 可使用 RMagick (選配)
+ text_destroy_time_entries_question: 您即將刪除的項目已報工 {{hours}} 小時. 您的選擇是?
+ text_destroy_time_entries: 刪除已報工的時數
+ text_assign_time_entries_to_project: 指定已報工的時數至專案中
+ text_reassign_time_entries: '重新指定已報工的時數至此項目:'
+ text_user_wrote: "{{value}} 先前提到:"
+ text_enumeration_destroy_question: "目前有 {{count}} 個物件使用此列舉值。"
+ text_enumeration_category_reassign_to: '重新設定其列舉值為:'
+ text_email_delivery_not_configured: "您尚未設定電子郵件傳送方式,因此提醒選項已被停用。\n請在 config/email.yml 中設定 SMTP 之後,重新啟動 Redmine,以啟用電子郵件提醒選項。"
+ 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: "重新指定所有的子頁面之父頁面至此頁面"
+
+ default_role_manager: 管理人員
+ default_role_developper: 開發人員
+ 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: 系統活動
--- /dev/null
+# Chinese (China) translations for Ruby on Rails
+# by tsechingho (http://github.com/tsechingho)
+
+zh:
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%b%d日"
+ long: "%Y年%b%d日"
+ day_names: [星期天, 星期一, 星期二, 星期三, 星期四, 星期五, 星期六]
+ abbr_day_names: [日, 一, 二, 三, 四, 五, 六]
+ month_names: [~, 一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 九月, 十月, 十一月, 十二月]
+ abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月]
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%Y年%b%d日 %A %H:%M:%S"
+ time: "%H:%M"
+ 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_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}} 年以上"
+ prompts:
+ year: "年"
+ month: "月"
+ day: "日"
+ hour: "时"
+ minute: "分"
+ second: "秒"
+
+ 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: 1
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ one: "Byte"
+ other: "Bytes"
+ kb: "KB"
+ mb: "MB"
+ gb: "GB"
+ tb: "TB"
+
+ support:
+ array:
+ words_connector: ", "
+ two_words_connector: " 和 "
+ last_word_connector: ", 和 "
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "有 1 个错误发生导致「{{model}}」无法被保存。"
+ other: "有 {{count}} 个错误发生导致「{{model}}」无法被保存。"
+ body: "如下字段出现错误:"
+ messages:
+ inclusion: "不包含于列表中"
+ exclusion: "是保留关键字"
+ invalid: "是无效的"
+ confirmation: "与确认值不匹配"
+ accepted: "必须是可被接受的"
+ empty: "不能留空"
+ blank: "不能为空字符"
+ too_long: "过长(最长为 {{count}} 个字符)"
+ too_short: "过短(最短为 {{count}} 个字符)"
+ wrong_length: "长度非法(必须为 {{count}} 个字符)"
+ taken: "已经被使用"
+ not_a_number: "不是数字"
+ greater_than: "必须大于 {{count}}"
+ greater_than_or_equal_to: "必须大于或等于 {{count}}"
+ equal_to: "必须等于 {{count}}"
+ less_than: "必须小于 {{count}}"
+ less_than_or_equal_to: "必须小于或等于 {{count}}"
+ odd: "必须为单数"
+ even: "必须为双数"
+ greater_than_start_date: "必须在起始日期之后"
+ not_same_project: "不属于同一个项目"
+ circular_dependency: "此关联将导致循环依赖"
+
+ 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_email_sent: "邮件已成功发送到 {{value}}"
+ notice_email_error: "发送邮件时发生错误 ({{value}})"
+ notice_feeds_access_key_reseted: 您的RSS存取键已被重置。
+ notice_failed_to_save_issues: "{{count}} 个问题保存失败(共选择 {{total}} 个问题):{{ids}}."
+ notice_no_issue_selected: "未选择任何问题!请选择您要编辑的问题。"
+ notice_account_pending: "您的帐号已被成功创建,正在等待管理员的审核。"
+ notice_default_data_loaded: 成功载入默认设置。
+ notice_unable_delete_version: 无法删除版本
+
+ error_can_t_load_default_data: "无法载入默认设置:{{value}}"
+ error_scm_not_found: "版本库中不存在该条目和(或)其修订版本。"
+ error_scm_command_failed: "访问版本库时发生错误:{{value}}"
+ error_scm_annotate: "该条目不存在或无法追溯。"
+ error_issue_not_found_in_project: '问题不存在或不属于此项目'
+ error_no_tracker_in_project: 该项目未设定跟踪标签,请检查项目配置。
+ error_no_default_issue_status: 未设置默认的问题状态。请检查系统设置("管理" -> "问题状态")。
+ error_can_not_reopen_issue_on_closed_version: 该问题被关联到一个已经关闭的版本,因此无法重新打开。
+
+ 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}} 个问题需要尽快解决"
+ mail_body_reminder: "指派给您的 {{count}} 个问题需要在 {{days}} 天内完成:"
+ mail_subject_wiki_content_added: "'{{page}}' wiki页面已添加"
+ mail_body_wiki_content_added: "'{{page}}' wiki页面已由 {{author}} 添加。"
+ mail_subject_wiki_content_updated: "'{{page}}' wiki页面已更新"
+ mail_body_wiki_content_updated: "'{{page}}' wiki页面已由 {{author}} 更新。"
+
+ gui_validation_error: 1 个错误
+ gui_validation_error_plural: "{{count}} 个错误"
+
+ field_name: 名称
+ field_description: 描述
+ field_summary: 摘要
+ field_is_required: 必填
+ field_firstname: 名字
+ field_lastname: 姓氏
+ field_mail: 邮件地址
+ field_filename: 文件
+ field_filesize: 大小
+ field_downloads: 下载次数
+ field_author: 作者
+ field_created_on: 创建于
+ field_updated_on: 更新于
+ field_field_format: 格式
+ field_is_for_all: 用于所有项目
+ field_possible_values: 可能的值
+ field_regexp: 正则表达式
+ field_min_length: 最小长度
+ field_max_length: 最大长度
+ field_value: 值
+ field_category: 类别
+ field_title: 标题
+ field_project: 项目
+ field_issue: 问题
+ field_status: 状态
+ field_notes: 说明
+ field_is_closed: 已关闭的问题
+ field_is_default: 默认值
+ field_tracker: 跟踪
+ field_subject: 主题
+ field_due_date: 完成日期
+ field_assigned_to: 指派给
+ field_priority: 优先级
+ field_fixed_version: 目标版本
+ field_user: 用户
+ field_role: 角色
+ field_homepage: 主页
+ field_is_public: 公开
+ field_parent: 上级项目
+ field_is_in_chlog: 在更新日志中显示问题
+ 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_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: 根据此条件分组
+
+ 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: RSS 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_repositories_encodings: 版本库编码
+ setting_commit_logs_encoding: 提交注释的编码
+ 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_api_enabled: 启用用于接收邮件的服务
+ setting_mail_handler_api_key: API key
+ setting_sequential_project_identifiers: 顺序产生项目标识
+ setting_gravatar_enabled: 使用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: 非管理员用户新建项目时将被赋予的(在该项目中的)角色
+
+ permission_add_project: 新建项目
+ permission_edit_project: 编辑项目
+ permission_select_project_modules: 选择项目模块
+ permission_manage_members: 管理成员
+ permission_manage_versions: 管理版本
+ permission_manage_categories: 管理问题类别
+ permission_add_issues: 新建问题
+ permission_edit_issues: 更新问题
+ permission_manage_issue_relations: 管理问题关联
+ permission_add_issue_notes: 添加说明
+ permission_edit_issue_notes: 编辑说明
+ permission_edit_own_issue_notes: 编辑自己的说明
+ permission_move_issues: 移动问题
+ permission_delete_issues: 删除问题
+ permission_manage_public_queries: 管理公开的查询
+ permission_save_queries: 保存查询
+ permission_view_gantt: 查看甘特图
+ 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_manage_documents: 管理文档
+ 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: 删除自己的帖子
+
+ 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: 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_administration: 管理
+ label_login: 登录
+ label_logout: 退出
+ label_help: 帮助
+ label_reported_issues: 已报告的问题
+ label_assigned_to_me_issues: 指派给我的问题
+ label_last_login: 最后登录
+ label_registered_on: 注册于
+ label_activity: 活动
+ label_overall_activity: 全部活动
+ label_user_activity: "{{value}} 的活动"
+ label_new: 新建
+ label_logged_as: 登录为
+ label_environment: 环境
+ label_authentication: 认证
+ label_auth_source: 认证模式
+ label_auth_source_new: 新建认证模式
+ label_auth_source_plural: 认证模式
+ label_subproject_plural: 子项目
+ label_and_its_subprojects: "{{value}} 及其子项目"
+ label_min_max_length: 最小 - 最大 长度
+ label_list: 列表
+ label_date: 日期
+ label_integer: 整数
+ label_float: 浮点数
+ label_boolean: 布尔量
+ label_string: 文字
+ label_text: 长段文字
+ label_attribute: 属性
+ label_attribute_plural: 属性
+ label_download: "{{count}} 次下载"
+ label_download_plural: "{{count}} 次下载"
+ label_no_data: 没有任何数据可供显示
+ label_change_status: 变更状态
+ label_history: 历史记录
+ label_attachment: 文件
+ label_attachment_new: 新建文件
+ label_attachment_delete: 删除文件
+ label_attachment_plural: 文件
+ label_file_added: 文件已添加
+ label_report: 报表
+ label_report_plural: 报表
+ label_news: 新闻
+ label_news_new: 添加新闻
+ label_news_plural: 新闻
+ label_news_latest: 最近的新闻
+ label_news_view_all: 查看所有新闻
+ label_news_added: 新闻已添加
+ label_change_log: 更新日志
+ label_settings: 配置
+ label_overview: 概述
+ label_version: 版本
+ label_version_new: 新建版本
+ label_version_plural: 版本
+ label_confirmation: 确认
+ label_export_to: 导出
+ label_read: 读取...
+ label_public_projects: 公开的项目
+ label_open_issues: 打开
+ label_open_issues_plural: 打开
+ label_closed_issues: 已关闭
+ label_closed_issues_plural: 已关闭
+ label_x_open_issues_abbr_on_total:
+ zero: 0 打开 / {{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_modification: "{{count}} 个更新"
+ label_modification_plural: "{{count}} 个更新"
+ label_branch: 分支
+ label_tag: 标签
+ label_revision: 修订
+ label_revision_plural: 修订
+ label_associated_revisions: 相关修订版本
+ label_added: 已添加
+ label_modified: 已修改
+ label_copied: 已复制
+ label_renamed: 已重命名
+ label_deleted: 已删除
+ label_latest_revision: 最近的修订版本
+ label_latest_revision_plural: 最近的修订版本
+ label_view_revisions: 查看修订
+ label_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_f_hour: "{{value}} 小时"
+ label_f_hour_plural: "{{value}} 小时"
+ label_time_tracking: 时间跟踪
+ label_change_plural: 变更
+ label_statistics: 统计
+ label_commits_per_month: 每月提交次数
+ label_commits_per_author: 每用户提交次数
+ label_view_diff: 查看差别
+ label_diff_inline: 直列
+ label_diff_side_by_side: 并排
+ label_options: 选项
+ label_copy_workflow_from: 从以下项目复制工作流程
+ label_permissions_report: 权限报表
+ label_watched_issues: 跟踪的问题
+ label_related_issues: 相关的问题
+ label_applied_status: 应用后的状态
+ label_loading: 载入中...
+ label_relation_new: 新建关联
+ label_relation_delete: 删除关联
+ label_relates_to: 关联到
+ label_duplicates: 重复
+ label_duplicated_by: 与其重复
+ label_blocks: 阻挡
+ label_blocked_by: 被阻挡
+ label_precedes: 优先于
+ label_follows: 跟随于
+ label_end_to_start: 结束-开始
+ label_end_to_end: 结束-结束
+ label_start_to_start: 开始-开始
+ label_start_to_end: 开始-结束
+ label_stay_logged_in: 保持登录状态
+ label_disabled: 禁用
+ label_show_completed_versions: 显示已完成的版本
+ label_me: 我
+ label_board: 讨论区
+ label_board_new: 新建讨论区
+ label_board_plural: 讨论区
+ label_topic_plural: 主题
+ label_message_plural: 帖子
+ label_message_last: 最新的帖子
+ label_message_new: 新贴
+ label_message_posted: 发帖成功
+ label_reply_plural: 回复
+ label_send_information: 给用户发送帐号信息
+ label_year: 年
+ label_month: 月
+ label_week: 周
+ label_date_from: 从
+ label_date_to: 到
+ label_language_based: 根据用户的语言
+ label_sort_by: "根据 {{value}} 排序"
+ label_send_test_email: 发送测试邮件
+ label_feeds_access_key_created_on: "RSS 存取键是在 {{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_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: 耗时
+
+ button_login: 登录
+ button_submit: 提交
+ button_save: 保存
+ button_check_all: 全选
+ button_uncheck_all: 清除
+ button_delete: 删除
+ button_create: 创建
+ button_create_and_continue: 创建并继续
+ button_test: 测试
+ button_edit: 编辑
+ button_add: 新增
+ button_change: 修改
+ button_apply: 应用
+ button_clear: 清除
+ button_lock: 锁定
+ button_unlock: 解锁
+ button_download: 下载
+ button_list: 列表
+ button_view: 查看
+ button_move: 移动
+ button_back: 返回
+ button_cancel: 取消
+ button_activate: 激活
+ button_sort: 排序
+ button_log_time: 登记工时
+ button_rollback: 恢复到这个版本
+ button_watch: 跟踪
+ button_unwatch: 取消跟踪
+ button_reply: 回复
+ button_archive: 存档
+ button_unarchive: 取消存档
+ button_reset: 重置
+ button_rename: 重命名
+ button_change_password: 修改密码
+ button_copy: 复制
+ button_annotate: 追溯
+ button_update: 更新
+ button_configure: 配置
+ button_quote: 引用
+
+ status_active: 活动的
+ status_registered: 已注册
+ status_locked: 已锁定
+
+ 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_project_identifier_info: '只允许使用小写字母(a-z),数字和连字符(-)。<br />请注意,标识符保存后将不可修改。'
+ text_caracters_maximum: "最多 {{count}} 个字符。"
+ text_caracters_minimum: "至少需要 {{count}} 个字符。"
+ text_length_between: "长度必须在 {{min}} 到 {{max}} 个字符之间。"
+ text_tracker_no_workflow: 此跟踪标签未定义工作流程
+ text_unallowed_characters: 非法字符
+ text_comma_separated: 可以使用多个值(用逗号,分开)。
+ text_issues_ref_in_commit_messages: 在提交信息中引用和解决问题
+ text_issue_added: "问题 {{id}} 已由 {{author}} 提交。"
+ text_issue_updated: "问题 {{id}} 已由 {{author}} 更新。"
+ text_wiki_destroy_confirmation: 您确定要删除这个 wiki 及其所有内容吗?
+ text_issue_category_destroy_question: "有一些问题({{count}} 个)属于此类别。您想进行哪种操作?"
+ text_issue_category_destroy_assignments: 删除问题的所属类别(问题变为无类别)
+ text_issue_category_reassign_to: 为问题选择其它类别
+ text_user_mail_option: "对于没有选中的项目,您将只会收到您跟踪或参与的项目的通知(比如说,您是问题的报告者, 或被指派解决此问题)。"
+ text_no_configuration_data: "角色、跟踪标签、问题状态和工作流程还没有设置。\n强烈建议您先载入默认设置,然后在此基础上进行修改。"
+ text_load_default_configuration: 载入默认设置
+ text_status_changed_by_changeset: "已应用到变更列表 {{value}}."
+ text_issues_destroy_confirmation: '您确定要删除选中的问题吗?'
+ text_select_project_modules: '请选择此项目可以使用的模块:'
+ text_default_administrator_account_changed: 默认的管理员帐号已改变
+ text_file_repository_writable: 附件路径可写
+ text_plugin_assets_writable: 插件的附件路径可写
+ text_rmagick_available: RMagick 可用(可选的)
+ text_destroy_time_entries_question: 您要删除的问题已经上报了 {{hours}} 小时的工作量。您想进行那种操作?
+ text_destroy_time_entries: 删除上报的工作量
+ text_assign_time_entries_to_project: 将已上报的工作量提交到项目中
+ text_reassign_time_entries: '将已上报的工作量指定到此问题:'
+ text_user_wrote: "{{value}} 写到:"
+ text_enumeration_category_reassign_to: '将它们关联到新的枚举值:'
+ text_enumeration_destroy_question: "{{count}} 个对象被关联到了这个枚举值。"
+ text_email_delivery_not_configured: "邮件参数尚未配置,因此邮件通知功能已被禁用。\n请在config/email.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_reassign_children: 将子页面的上级页面设置为
+ text_wiki_page_nullify_children: 将子页面保留为根页面
+ text_wiki_page_destroy_children: 删除子页面及其所有下级页面
+
+ default_role_manager: 管理人员
+ default_role_developper: 开发人员
+ default_role_reporter: 报告人员
+ default_tracker_bug: 错误
+ default_tracker_feature: 功能
+ default_tracker_support: 支持
+ default_issue_status_new: 新建
+ default_issue_status_in_progress: In Progress
+ default_issue_status_resolved: 已解决
+ default_issue_status_feedback: 反馈
+ default_issue_status_closed: 已关闭
+ default_issue_status_rejected: 已拒绝
+ default_doc_category_user: 用户文档
+ default_doc_category_tech: 技术文档
+ default_priority_low: 低
+ default_priority_normal: 普通
+ default_priority_high: 高
+ default_priority_urgent: 紧急
+ default_priority_immediate: 立刻
+ default_activity_design: 设计
+ default_activity_development: 开发
+
+ enumeration_issue_priorities: 问题优先级
+ enumeration_doc_categories: 文档类别
+ enumeration_activities: 活动(时间跟踪)
+ enumeration_system_activity: 系统活动
+ button_move_and_follow: Move and follow
+ setting_default_projects_modules: Default enabled modules for new projects
+ setting_gravatar_default: Default Gravatar image
--- /dev/null
+ActionController::Routing::Routes.draw do |map|
+ # Add your own custom routes here.
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Here's a sample route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ map.home '', :controller => 'welcome'
+
+ map.signin 'login', :controller => 'account', :action => 'login'
+ map.signout 'logout', :controller => 'account', :action => 'logout'
+
+ map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
+ map.connect 'help/:ctrl/:page', :controller => 'help'
+
+ map.connect 'time_entries/:id/edit', :action => 'edit', :controller => 'timelog'
+ map.connect 'projects/:project_id/time_entries/new', :action => 'edit', :controller => 'timelog'
+ map.connect 'projects/:project_id/issues/:issue_id/time_entries/new', :action => 'edit', :controller => 'timelog'
+
+ map.with_options :controller => 'timelog' do |timelog|
+ timelog.connect 'projects/:project_id/time_entries', :action => 'details'
+
+ timelog.with_options :action => 'details', :conditions => {:method => :get} do |time_details|
+ time_details.connect 'time_entries'
+ time_details.connect 'time_entries.:format'
+ time_details.connect 'issues/:issue_id/time_entries'
+ time_details.connect 'issues/:issue_id/time_entries.:format'
+ time_details.connect 'projects/:project_id/time_entries.:format'
+ time_details.connect 'projects/:project_id/issues/:issue_id/time_entries'
+ time_details.connect 'projects/:project_id/issues/:issue_id/time_entries.:format'
+ end
+ timelog.connect 'projects/:project_id/time_entries/report', :action => 'report'
+ timelog.with_options :action => 'report',:conditions => {:method => :get} do |time_report|
+ time_report.connect 'time_entries/report'
+ time_report.connect 'time_entries/report.:format'
+ time_report.connect 'projects/:project_id/time_entries/report.:format'
+ end
+
+ timelog.with_options :action => 'edit', :conditions => {:method => :get} do |time_edit|
+ time_edit.connect 'issues/:issue_id/time_entries/new'
+ end
+
+ timelog.connect 'time_entries/:id/destroy', :action => 'destroy', :conditions => {:method => :post}
+ end
+
+ map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
+ map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
+ map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
+ map.with_options :controller => 'wiki' do |wiki_routes|
+ wiki_routes.with_options :conditions => {:method => :get} do |wiki_views|
+ wiki_views.connect 'projects/:id/wiki/:page', :action => 'special', :page => /page_index|date_index|export/i
+ wiki_views.connect 'projects/:id/wiki/:page', :action => 'index', :page => nil
+ wiki_views.connect 'projects/:id/wiki/:page/edit', :action => 'edit'
+ wiki_views.connect 'projects/:id/wiki/:page/rename', :action => 'rename'
+ wiki_views.connect 'projects/:id/wiki/:page/history', :action => 'history'
+ wiki_views.connect 'projects/:id/wiki/:page/diff/:version/vs/:version_from', :action => 'diff'
+ wiki_views.connect 'projects/:id/wiki/:page/annotate/:version', :action => 'annotate'
+ end
+
+ wiki_routes.connect 'projects/:id/wiki/:page/:action',
+ :action => /edit|rename|destroy|preview|protect/,
+ :conditions => {:method => :post}
+ end
+
+ map.with_options :controller => 'messages' do |messages_routes|
+ messages_routes.with_options :conditions => {:method => :get} do |messages_views|
+ messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
+ messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
+ messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
+ end
+ messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
+ messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
+ messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
+ messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
+ end
+ end
+
+ map.with_options :controller => 'boards' do |board_routes|
+ board_routes.with_options :conditions => {:method => :get} do |board_views|
+ board_views.connect 'projects/:project_id/boards', :action => 'index'
+ board_views.connect 'projects/:project_id/boards/new', :action => 'new'
+ board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
+ board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
+ board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
+ end
+ board_routes.with_options :conditions => {:method => :post} do |board_actions|
+ board_actions.connect 'projects/:project_id/boards', :action => 'new'
+ board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
+ end
+ end
+
+ map.with_options :controller => 'documents' do |document_routes|
+ document_routes.with_options :conditions => {:method => :get} do |document_views|
+ document_views.connect 'projects/:project_id/documents', :action => 'index'
+ document_views.connect 'projects/:project_id/documents/new', :action => 'new'
+ document_views.connect 'documents/:id', :action => 'show'
+ document_views.connect 'documents/:id/edit', :action => 'edit'
+ end
+ document_routes.with_options :conditions => {:method => :post} do |document_actions|
+ document_actions.connect 'projects/:project_id/documents', :action => 'new'
+ document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
+ end
+ end
+
+ map.with_options :controller => 'issues' do |issues_routes|
+ issues_routes.with_options :conditions => {:method => :get} do |issues_views|
+ issues_views.connect 'issues', :action => 'index'
+ issues_views.connect 'issues.:format', :action => 'index'
+ issues_views.connect 'projects/:project_id/issues', :action => 'index'
+ issues_views.connect 'projects/:project_id/issues.:format', :action => 'index'
+ issues_views.connect 'projects/:project_id/issues/new', :action => 'new'
+ issues_views.connect 'projects/:project_id/issues/gantt', :action => 'gantt'
+ issues_views.connect 'projects/:project_id/issues/calendar', :action => 'calendar'
+ issues_views.connect 'projects/:project_id/issues/:copy_from/copy', :action => 'new'
+ issues_views.connect 'issues/:id', :action => 'show', :id => /\d+/
+ issues_views.connect 'issues/:id.:format', :action => 'show', :id => /\d+/
+ issues_views.connect 'issues/:id/edit', :action => 'edit', :id => /\d+/
+ issues_views.connect 'issues/:id/move', :action => 'move', :id => /\d+/
+ end
+ issues_routes.with_options :conditions => {:method => :post} do |issues_actions|
+ issues_actions.connect 'projects/:project_id/issues', :action => 'new'
+ issues_actions.connect 'issues/:id/quoted', :action => 'reply', :id => /\d+/
+ issues_actions.connect 'issues/:id/:action', :action => /edit|move|destroy/, :id => /\d+/
+ end
+ issues_routes.connect 'issues/:action'
+ end
+
+ map.with_options :controller => 'issue_relations', :conditions => {:method => :post} do |relations|
+ relations.connect 'issues/:issue_id/relations/:id', :action => 'new'
+ relations.connect 'issues/:issue_id/relations/:id/destroy', :action => 'destroy'
+ end
+
+ map.with_options :controller => 'reports', :action => 'issue_report', :conditions => {:method => :get} do |reports|
+ reports.connect 'projects/:id/issues/report'
+ reports.connect 'projects/:id/issues/report/:detail'
+ end
+
+ map.with_options :controller => 'news' do |news_routes|
+ news_routes.with_options :conditions => {:method => :get} do |news_views|
+ news_views.connect 'news', :action => 'index'
+ news_views.connect 'projects/:project_id/news', :action => 'index'
+ news_views.connect 'projects/:project_id/news.:format', :action => 'index'
+ news_views.connect 'news.:format', :action => 'index'
+ news_views.connect 'projects/:project_id/news/new', :action => 'new'
+ news_views.connect 'news/:id', :action => 'show'
+ news_views.connect 'news/:id/edit', :action => 'edit'
+ end
+ news_routes.with_options do |news_actions|
+ news_actions.connect 'projects/:project_id/news', :action => 'new'
+ news_actions.connect 'news/:id/edit', :action => 'edit'
+ news_actions.connect 'news/:id/destroy', :action => 'destroy'
+ end
+ end
+
+ map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
+
+ map.with_options :controller => 'users' do |users|
+ users.with_options :conditions => {:method => :get} do |user_views|
+ user_views.connect 'users', :action => 'index'
+ user_views.connect 'users/:id', :action => 'show', :id => /\d+/
+ user_views.connect 'users/new', :action => 'add'
+ user_views.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil
+ end
+ users.with_options :conditions => {:method => :post} do |user_actions|
+ user_actions.connect 'users', :action => 'add'
+ user_actions.connect 'users/new', :action => 'add'
+ user_actions.connect 'users/:id/edit', :action => 'edit'
+ user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
+ user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
+ user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
+ end
+ end
+
+ map.with_options :controller => 'projects' do |projects|
+ projects.with_options :conditions => {:method => :get} do |project_views|
+ project_views.connect 'projects', :action => 'index'
+ project_views.connect 'projects.:format', :action => 'index'
+ project_views.connect 'projects/new', :action => 'add'
+ project_views.connect 'projects/:id', :action => 'show'
+ project_views.connect 'projects/:id/:action', :action => /roadmap|changelog|destroy|settings/
+ project_views.connect 'projects/:id/files', :action => 'list_files'
+ project_views.connect 'projects/:id/files/new', :action => 'add_file'
+ project_views.connect 'projects/:id/versions/new', :action => 'add_version'
+ project_views.connect 'projects/:id/categories/new', :action => 'add_issue_category'
+ project_views.connect 'projects/:id/settings/:tab', :action => 'settings'
+ end
+
+ projects.with_options :action => 'activity', :conditions => {:method => :get} do |activity|
+ activity.connect 'projects/:id/activity'
+ activity.connect 'projects/:id/activity.:format'
+ activity.connect 'activity', :id => nil
+ activity.connect 'activity.:format', :id => nil
+ end
+
+ projects.with_options :conditions => {:method => :post} do |project_actions|
+ project_actions.connect 'projects/new', :action => 'add'
+ project_actions.connect 'projects', :action => 'add'
+ project_actions.connect 'projects/:id/:action', :action => /destroy|archive|unarchive/
+ project_actions.connect 'projects/:id/files/new', :action => 'add_file'
+ project_actions.connect 'projects/:id/versions/new', :action => 'add_version'
+ project_actions.connect 'projects/:id/categories/new', :action => 'add_issue_category'
+ project_actions.connect 'projects/:id/activities/save', :action => 'save_activities'
+ end
+
+ projects.with_options :conditions => {:method => :delete} do |project_actions|
+ project_actions.conditions 'projects/:id/reset_activities', :action => 'reset_activities'
+ end
+ end
+
+ map.with_options :controller => 'versions' do |versions|
+ versions.with_options :conditions => {:method => :post} do |version_actions|
+ version_actions.connect 'projects/:project_id/versions/close_completed', :action => 'close_completed'
+ end
+ end
+
+ map.with_options :controller => 'repositories' do |repositories|
+ repositories.with_options :conditions => {:method => :get} do |repository_views|
+ repository_views.connect 'projects/:id/repository', :action => 'show'
+ repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
+ repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
+ repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
+ repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
+ repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
+ repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
+ repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
+ repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
+ repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
+ repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
+ # TODO: why the following route is required?
+ repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
+ repository_views.connect 'projects/:id/repository/:action/*path'
+ end
+
+ repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
+ end
+
+ map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
+ map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
+ map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
+
+ map.resources :groups
+
+ #left old routes at the bottom for backwards compat
+ map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
+ map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
+ map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
+ map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
+ map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
+ map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
+ map.connect 'projects/:project_id/news/:action', :controller => 'news'
+ map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
+ map.with_options :controller => 'repositories' do |omap|
+ omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
+ omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
+ omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
+ omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
+ omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
+ omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
+ end
+
+ map.with_options :controller => 'sys' do |sys|
+ sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
+ sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
+ end
+
+ # Install the default route as the lowest priority.
+ map.connect ':controller/:action/:id'
+ map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
+ # Used for OpenID
+ map.root :controller => 'account', :action => 'login'
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+# DO NOT MODIFY THIS FILE !!!
+# Settings can be defined through the application in Admin -> Settings
+
+app_title:
+ default: Redmine
+app_subtitle:
+ default: Project management
+welcome_text:
+ default:
+login_required:
+ default: 0
+self_registration:
+ default: '2'
+lost_password:
+ default: 1
+password_min_length:
+ format: int
+ default: 4
+attachment_max_size:
+ format: int
+ default: 5120
+issues_export_limit:
+ format: int
+ default: 500
+activity_days_default:
+ format: int
+ default: 30
+per_page_options:
+ default: '25,50,100'
+mail_from:
+ default: redmine@example.net
+bcc_recipients:
+ default: 1
+plain_text_mail:
+ default: 0
+text_formatting:
+ default: textile
+wiki_compression:
+ default: ""
+default_language:
+ default: en
+host_name:
+ default: localhost:3000
+protocol:
+ default: http
+feeds_limit:
+ format: int
+ default: 15
+# Maximum size of files that can be displayed
+# inline through the file viewer (in KB)
+file_max_size_displayed:
+ format: int
+ default: 512
+diff_max_lines_displayed:
+ format: int
+ default: 1500
+enabled_scm:
+ serialized: true
+ default:
+ - Subversion
+ - Darcs
+ - Mercurial
+ - Cvs
+ - Bazaar
+ - Git
+autofetch_changesets:
+ default: 1
+sys_api_enabled:
+ default: 0
+commit_ref_keywords:
+ default: 'refs,references,IssueID'
+commit_fix_keywords:
+ default: 'fixes,closes'
+commit_fix_status_id:
+ format: int
+ default: 0
+commit_fix_done_ratio:
+ default: 100
+# autologin duration in days
+# 0 means autologin is disabled
+autologin:
+ format: int
+ default: 0
+# date format
+date_format:
+ default: ''
+time_format:
+ default: ''
+user_format:
+ default: :firstname_lastname
+ format: symbol
+cross_project_issue_relations:
+ default: 0
+notified_events:
+ serialized: true
+ default:
+ - issue_added
+ - issue_updated
+mail_handler_api_enabled:
+ default: 0
+mail_handler_api_key:
+ default:
+issue_list_default_columns:
+ serialized: true
+ default:
+ - tracker
+ - status
+ - priority
+ - subject
+ - assigned_to
+ - updated_on
+display_subprojects_issues:
+ default: 1
+default_projects_public:
+ default: 1
+default_projects_modules:
+ serialized: true
+ default:
+ - issue_tracking
+ - time_tracking
+ - news
+ - documents
+ - files
+ - wiki
+ - repository
+ - boards
+# Role given to a non-admin user who creates a project
+new_project_user_role_id:
+ format: int
+ default: ''
+sequential_project_identifiers:
+ default: 0
+# encodings used to convert repository files content to UTF-8
+# multiple values accepted, comma separated
+repositories_encodings:
+ default: ''
+# encoding used to convert commit logs to UTF-8
+commit_logs_encoding:
+ default: 'UTF-8'
+repository_log_display_limit:
+ format: int
+ default: 100
+ui_theme:
+ default: ''
+emails_footer:
+ default: |-
+ You have received this notification because you have either subscribed to it, or are involved in it.
+ To change your notification preferences, please click here: http://hostname/my/account
+gravatar_enabled:
+ default: 0
+openid:
+ default: 0
+gravatar_default:
+ default: ''
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Setup < ActiveRecord::Migration
+
+ class User < ActiveRecord::Base; end
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up\r
+ create_table "attachments", :force => true do |t|\r
+ t.column "container_id", :integer, :default => 0, :null => false\r
+ t.column "container_type", :string, :limit => 30, :default => "", :null => false\r
+ t.column "filename", :string, :default => "", :null => false\r
+ t.column "disk_filename", :string, :default => "", :null => false\r
+ t.column "filesize", :integer, :default => 0, :null => false\r
+ t.column "content_type", :string, :limit => 60, :default => ""\r
+ t.column "digest", :string, :limit => 40, :default => "", :null => false\r
+ t.column "downloads", :integer, :default => 0, :null => false\r
+ t.column "author_id", :integer, :default => 0, :null => false\r
+ t.column "created_on", :timestamp\r
+ end\r
+
+ create_table "auth_sources", :force => true do |t|
+ t.column "type", :string, :limit => 30, :default => "", :null => false
+ t.column "name", :string, :limit => 60, :default => "", :null => false
+ t.column "host", :string, :limit => 60
+ t.column "port", :integer
+ t.column "account", :string, :limit => 60
+ t.column "account_password", :string, :limit => 60
+ t.column "base_dn", :string, :limit => 255
+ t.column "attr_login", :string, :limit => 30
+ t.column "attr_firstname", :string, :limit => 30
+ t.column "attr_lastname", :string, :limit => 30
+ t.column "attr_mail", :string, :limit => 30
+ t.column "onthefly_register", :boolean, :default => false, :null => false
+ end
+ \r
+ create_table "custom_fields", :force => true do |t|
+ t.column "type", :string, :limit => 30, :default => "", :null => false\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ t.column "field_format", :string, :limit => 30, :default => "", :null => false\r
+ t.column "possible_values", :text\r
+ t.column "regexp", :string, :default => ""\r
+ t.column "min_length", :integer, :default => 0, :null => false\r
+ t.column "max_length", :integer, :default => 0, :null => false\r
+ t.column "is_required", :boolean, :default => false, :null => false
+ t.column "is_for_all", :boolean, :default => false, :null => false
+ end\r
+ \r
+ create_table "custom_fields_projects", :id => false, :force => true do |t|\r
+ t.column "custom_field_id", :integer, :default => 0, :null => false\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ end\r
+
+ create_table "custom_fields_trackers", :id => false, :force => true do |t|
+ t.column "custom_field_id", :integer, :default => 0, :null => false
+ t.column "tracker_id", :integer, :default => 0, :null => false
+ end
+\r
+ create_table "custom_values", :force => true do |t|\r
+ t.column "customized_type", :string, :limit => 30, :default => "", :null => false
+ t.column "customized_id", :integer, :default => 0, :null => false
+ t.column "custom_field_id", :integer, :default => 0, :null => false\r
+ t.column "value", :text\r
+ end\r
+ \r
+ create_table "documents", :force => true do |t|\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ t.column "category_id", :integer, :default => 0, :null => false\r
+ t.column "title", :string, :limit => 60, :default => "", :null => false\r
+ t.column "description", :text\r
+ t.column "created_on", :timestamp\r
+ end
+
+ add_index "documents", ["project_id"], :name => "documents_project_id"\r
+ \r
+ create_table "enumerations", :force => true do |t|\r
+ t.column "opt", :string, :limit => 4, :default => "", :null => false\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ end\r
+ \r
+ create_table "issue_categories", :force => true do |t|\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ end
+
+ add_index "issue_categories", ["project_id"], :name => "issue_categories_project_id"\r
+ \r
+ create_table "issue_histories", :force => true do |t|\r
+ t.column "issue_id", :integer, :default => 0, :null => false\r
+ t.column "status_id", :integer, :default => 0, :null => false\r
+ t.column "author_id", :integer, :default => 0, :null => false\r
+ t.column "notes", :text\r
+ t.column "created_on", :timestamp\r
+ end\r
+ \r
+ add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id"\r
+\r
+ create_table "issue_statuses", :force => true do |t|\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ t.column "is_closed", :boolean, :default => false, :null => false\r
+ t.column "is_default", :boolean, :default => false, :null => false\r
+ t.column "html_color", :string, :limit => 6, :default => "FFFFFF", :null => false\r
+ end\r
+ \r
+ create_table "issues", :force => true do |t|\r
+ t.column "tracker_id", :integer, :default => 0, :null => false\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ t.column "subject", :string, :default => "", :null => false\r
+ t.column "description", :text
+ t.column "due_date", :date\r
+ t.column "category_id", :integer\r
+ t.column "status_id", :integer, :default => 0, :null => false\r
+ t.column "assigned_to_id", :integer\r
+ t.column "priority_id", :integer, :default => 0, :null => false\r
+ t.column "fixed_version_id", :integer\r
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "lock_version", :integer, :default => 0, :null => false\r
+ t.column "created_on", :timestamp\r
+ t.column "updated_on", :timestamp\r
+ end\r
+ \r
+ add_index "issues", ["project_id"], :name => "issues_project_id"\r
+ \r
+ create_table "members", :force => true do |t|\r
+ t.column "user_id", :integer, :default => 0, :null => false\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ t.column "role_id", :integer, :default => 0, :null => false\r
+ t.column "created_on", :timestamp\r
+ end\r
+ \r
+ create_table "news", :force => true do |t|\r
+ t.column "project_id", :integer\r
+ t.column "title", :string, :limit => 60, :default => "", :null => false\r
+ t.column "summary", :string, :limit => 255, :default => ""\r
+ t.column "description", :text\r
+ t.column "author_id", :integer, :default => 0, :null => false\r
+ t.column "created_on", :timestamp\r
+ end
+
+ add_index "news", ["project_id"], :name => "news_project_id"\r
+ \r
+ create_table "permissions", :force => true do |t|\r
+ t.column "controller", :string, :limit => 30, :default => "", :null => false\r
+ t.column "action", :string, :limit => 30, :default => "", :null => false\r
+ t.column "description", :string, :limit => 60, :default => "", :null => false\r
+ t.column "is_public", :boolean, :default => false, :null => false\r
+ t.column "sort", :integer, :default => 0, :null => false\r
+ t.column "mail_option", :boolean, :default => false, :null => false\r
+ t.column "mail_enabled", :boolean, :default => false, :null => false\r
+ end\r
+ \r
+ create_table "permissions_roles", :id => false, :force => true do |t|\r
+ t.column "permission_id", :integer, :default => 0, :null => false\r
+ t.column "role_id", :integer, :default => 0, :null => false\r
+ end\r
+ \r
+ add_index "permissions_roles", ["role_id"], :name => "permissions_roles_role_id"\r
+ \r
+ create_table "projects", :force => true do |t|\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ t.column "description", :string, :default => "", :null => false\r
+ t.column "homepage", :string, :limit => 60, :default => ""\r
+ t.column "is_public", :boolean, :default => true, :null => false
+ t.column "parent_id", :integer
+ t.column "projects_count", :integer, :default => 0\r
+ t.column "created_on", :timestamp\r
+ t.column "updated_on", :timestamp\r
+ end\r
+ \r
+ create_table "roles", :force => true do |t|\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ end\r
+
+ create_table "tokens", :force => true do |t|
+ t.column "user_id", :integer, :default => 0, :null => false
+ t.column "action", :string, :limit => 30, :default => "", :null => false
+ t.column "value", :string, :limit => 40, :default => "", :null => false
+ t.column "created_on", :datetime, :null => false
+ end
+ \r
+ create_table "trackers", :force => true do |t|\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ t.column "is_in_chlog", :boolean, :default => false, :null => false\r
+ end\r
+ \r
+ create_table "users", :force => true do |t|\r
+ t.column "login", :string, :limit => 30, :default => "", :null => false\r
+ t.column "hashed_password", :string, :limit => 40, :default => "", :null => false\r
+ t.column "firstname", :string, :limit => 30, :default => "", :null => false\r
+ t.column "lastname", :string, :limit => 30, :default => "", :null => false\r
+ t.column "mail", :string, :limit => 60, :default => "", :null => false\r
+ t.column "mail_notification", :boolean, :default => true, :null => false\r
+ t.column "admin", :boolean, :default => false, :null => false\r
+ t.column "status", :integer, :default => 1, :null => false\r
+ t.column "last_login_on", :datetime\r
+ t.column "language", :string, :limit => 2, :default => ""
+ t.column "auth_source_id", :integer\r
+ t.column "created_on", :timestamp\r
+ t.column "updated_on", :timestamp\r
+ end\r
+ \r
+ create_table "versions", :force => true do |t|\r
+ t.column "project_id", :integer, :default => 0, :null => false\r
+ t.column "name", :string, :limit => 30, :default => "", :null => false\r
+ t.column "description", :string, :default => ""\r
+ t.column "effective_date", :date\r
+ t.column "created_on", :timestamp\r
+ t.column "updated_on", :timestamp\r
+ end
+
+ add_index "versions", ["project_id"], :name => "versions_project_id"\r
+ \r
+ create_table "workflows", :force => true do |t|\r
+ t.column "tracker_id", :integer, :default => 0, :null => false\r
+ t.column "old_status_id", :integer, :default => 0, :null => false\r
+ t.column "new_status_id", :integer, :default => 0, :null => false\r
+ t.column "role_id", :integer, :default => 0, :null => false\r
+ end\r
+ \r
+ # project\r
+ Permission.create :controller => "projects", :action => "show", :description => "label_overview", :sort => 100, :is_public => true\r
+ Permission.create :controller => "projects", :action => "changelog", :description => "label_change_log", :sort => 105, :is_public => true\r
+ Permission.create :controller => "reports", :action => "issue_report", :description => "label_report_plural", :sort => 110, :is_public => true\r
+ Permission.create :controller => "projects", :action => "settings", :description => "label_settings", :sort => 150\r
+ Permission.create :controller => "projects", :action => "edit", :description => "button_edit", :sort => 151\r
+ # members\r
+ Permission.create :controller => "projects", :action => "list_members", :description => "button_list", :sort => 200, :is_public => true\r
+ Permission.create :controller => "projects", :action => "add_member", :description => "button_add", :sort => 220\r
+ Permission.create :controller => "members", :action => "edit", :description => "button_edit", :sort => 221\r
+ Permission.create :controller => "members", :action => "destroy", :description => "button_delete", :sort => 222\r
+ # versions\r
+ Permission.create :controller => "projects", :action => "add_version", :description => "button_add", :sort => 320\r
+ Permission.create :controller => "versions", :action => "edit", :description => "button_edit", :sort => 321\r
+ Permission.create :controller => "versions", :action => "destroy", :description => "button_delete", :sort => 322\r
+ # issue categories\r
+ Permission.create :controller => "projects", :action => "add_issue_category", :description => "button_add", :sort => 420\r
+ Permission.create :controller => "issue_categories", :action => "edit", :description => "button_edit", :sort => 421\r
+ Permission.create :controller => "issue_categories", :action => "destroy", :description => "button_delete", :sort => 422\r
+ # issues\r
+ Permission.create :controller => "projects", :action => "list_issues", :description => "button_list", :sort => 1000, :is_public => true\r
+ Permission.create :controller => "projects", :action => "export_issues_csv", :description => "label_export_csv", :sort => 1001, :is_public => true
+ Permission.create :controller => "issues", :action => "show", :description => "button_view", :sort => 1005, :is_public => true\r
+ Permission.create :controller => "issues", :action => "download", :description => "button_download", :sort => 1010, :is_public => true\r
+ Permission.create :controller => "projects", :action => "add_issue", :description => "button_add", :sort => 1050, :mail_option => 1, :mail_enabled => 1\r
+ Permission.create :controller => "issues", :action => "edit", :description => "button_edit", :sort => 1055\r
+ Permission.create :controller => "issues", :action => "change_status", :description => "label_change_status", :sort => 1060, :mail_option => 1, :mail_enabled => 1\r
+ Permission.create :controller => "issues", :action => "destroy", :description => "button_delete", :sort => 1065\r
+ Permission.create :controller => "issues", :action => "add_attachment", :description => "label_attachment_new", :sort => 1070\r
+ Permission.create :controller => "issues", :action => "destroy_attachment", :description => "label_attachment_delete", :sort => 1075\r
+ # news\r
+ Permission.create :controller => "projects", :action => "list_news", :description => "button_list", :sort => 1100, :is_public => true\r
+ Permission.create :controller => "news", :action => "show", :description => "button_view", :sort => 1101, :is_public => true\r
+ Permission.create :controller => "projects", :action => "add_news", :description => "button_add", :sort => 1120\r
+ Permission.create :controller => "news", :action => "edit", :description => "button_edit", :sort => 1121\r
+ Permission.create :controller => "news", :action => "destroy", :description => "button_delete", :sort => 1122\r
+ # documents
+ Permission.create :controller => "projects", :action => "list_documents", :description => "button_list", :sort => 1200, :is_public => true\r
+ Permission.create :controller => "documents", :action => "show", :description => "button_view", :sort => 1201, :is_public => true\r
+ Permission.create :controller => "documents", :action => "download", :description => "button_download", :sort => 1202, :is_public => true\r
+ Permission.create :controller => "projects", :action => "add_document", :description => "button_add", :sort => 1220\r
+ Permission.create :controller => "documents", :action => "edit", :description => "button_edit", :sort => 1221\r
+ Permission.create :controller => "documents", :action => "destroy", :description => "button_delete", :sort => 1222\r
+ Permission.create :controller => "documents", :action => "add_attachment", :description => "label_attachment_new", :sort => 1223\r
+ Permission.create :controller => "documents", :action => "destroy_attachment", :description => "label_attachment_delete", :sort => 1224\r
+ # files\r
+ Permission.create :controller => "projects", :action => "list_files", :description => "button_list", :sort => 1300, :is_public => true\r
+ Permission.create :controller => "versions", :action => "download", :description => "button_download", :sort => 1301, :is_public => true\r
+ Permission.create :controller => "projects", :action => "add_file", :description => "button_add", :sort => 1320\r
+ Permission.create :controller => "versions", :action => "destroy_file", :description => "button_delete", :sort => 1322\r
+ \r
+ # create default administrator account\r
+ user = User.create :login => "admin",
+ :hashed_password => "d033e22ae348aeb5660fc2140aec35850c4da997",
+ :admin => true,
+ :firstname => "Redmine",
+ :lastname => "Admin",
+ :mail => "admin@example.net",
+ :mail_notification => true,
+ :language => "en",
+ :status => 1
+ end
+
+ def self.down\r
+ drop_table :attachments
+ drop_table :auth_sources\r
+ drop_table :custom_fields\r
+ drop_table :custom_fields_projects
+ drop_table :custom_fields_trackers\r
+ drop_table :custom_values\r
+ drop_table :documents\r
+ drop_table :enumerations\r
+ drop_table :issue_categories\r
+ drop_table :issue_histories\r
+ drop_table :issue_statuses\r
+ drop_table :issues\r
+ drop_table :members\r
+ drop_table :news\r
+ drop_table :permissions\r
+ drop_table :permissions_roles\r
+ drop_table :projects\r
+ drop_table :roles\r
+ drop_table :trackers\r
+ drop_table :tokens
+ drop_table :users\r
+ drop_table :versions\r
+ drop_table :workflows
+ end
+end
--- /dev/null
+class IssueMove < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "move_issues", :description => "button_move", :sort => 1061, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'move_issues']).destroy
+ end
+end
--- /dev/null
+class IssueAddNote < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "issues", :action => "add_note", :description => "label_add_note", :sort => 1057, :mail_option => 1, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'add_note']).destroy
+ end
+end
--- /dev/null
+class ExportPdf < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "export_issues_pdf", :description => "label_export_pdf", :sort => 1002, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "issues", :action => "export_pdf", :description => "label_export_pdf", :sort => 1015, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'export_issues_pdf']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'export_pdf']).destroy
+ end
+end
--- /dev/null
+class IssueStartDate < ActiveRecord::Migration
+ def self.up
+ add_column :issues, :start_date, :date
+ add_column :issues, :done_ratio, :integer, :default => 0, :null => false
+ end
+
+ def self.down
+ remove_column :issues, :start_date
+ remove_column :issues, :done_ratio
+ end
+end
--- /dev/null
+class CalendarAndActivity < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "activity", :description => "label_activity", :sort => 160, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "projects", :action => "calendar", :description => "label_calendar", :sort => 165, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "projects", :action => "gantt", :description => "label_gantt", :sort => 166, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'activity']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'calendar']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'gantt']).destroy
+ end
+end
--- /dev/null
+class CreateJournals < ActiveRecord::Migration
+
+ # model removed, but needed for data migration
+ class IssueHistory < ActiveRecord::Base; belongs_to :issue; end
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ create_table :journals, :force => true do |t|
+ t.column "journalized_id", :integer, :default => 0, :null => false
+ t.column "journalized_type", :string, :limit => 30, :default => "", :null => false
+ t.column "user_id", :integer, :default => 0, :null => false
+ t.column "notes", :text
+ t.column "created_on", :datetime, :null => false
+ end
+ create_table :journal_details, :force => true do |t|
+ t.column "journal_id", :integer, :default => 0, :null => false
+ t.column "property", :string, :limit => 30, :default => "", :null => false
+ t.column "prop_key", :string, :limit => 30, :default => "", :null => false
+ t.column "old_value", :string
+ t.column "value", :string
+ end
+
+ # indexes
+ add_index "journals", ["journalized_id", "journalized_type"], :name => "journals_journalized_id"
+ add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id"
+
+ Permission.create :controller => "issues", :action => "history", :description => "label_history", :sort => 1006, :is_public => true, :mail_option => 0, :mail_enabled => 0
+
+ # data migration
+ IssueHistory.find(:all, :include => :issue).each {|h|
+ j = Journal.new(:journalized => h.issue, :user_id => h.author_id, :notes => h.notes, :created_on => h.created_on)
+ j.details << JournalDetail.new(:property => 'attr', :prop_key => 'status_id', :value => h.status_id)
+ j.save
+ }
+
+ drop_table :issue_histories
+ end
+
+ def self.down
+ drop_table :journal_details
+ drop_table :journals
+
+ create_table "issue_histories", :force => true do |t|
+ t.column "issue_id", :integer, :default => 0, :null => false
+ t.column "status_id", :integer, :default => 0, :null => false
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "notes", :text, :default => ""
+ t.column "created_on", :timestamp
+ end
+
+ add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id"
+
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'history']).destroy
+ end
+end
--- /dev/null
+class CreateUserPreferences < ActiveRecord::Migration
+ def self.up
+ create_table :user_preferences do |t|
+ t.column "user_id", :integer, :default => 0, :null => false
+ t.column "others", :text
+ end
+ end
+
+ def self.down
+ drop_table :user_preferences
+ end
+end
--- /dev/null
+class AddHideMailPref < ActiveRecord::Migration
+ def self.up
+ add_column :user_preferences, :hide_mail, :boolean, :default => false
+ end
+
+ def self.down
+ remove_column :user_preferences, :hide_mail
+ end
+end
--- /dev/null
+class CreateComments < ActiveRecord::Migration
+ def self.up
+ create_table :comments do |t|
+ t.column :commented_type, :string, :limit => 30, :default => "", :null => false
+ t.column :commented_id, :integer, :default => 0, :null => false
+ t.column :author_id, :integer, :default => 0, :null => false
+ t.column :comments, :text
+ t.column :created_on, :datetime, :null => false
+ t.column :updated_on, :datetime, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :comments
+ end
+end
--- /dev/null
+class AddNewsCommentsCount < ActiveRecord::Migration
+ def self.up
+ add_column :news, :comments_count, :integer, :default => 0, :null => false
+ end
+
+ def self.down
+ remove_column :news, :comments_count
+ end
+end
--- /dev/null
+class AddCommentsPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "news", :action => "add_comment", :description => "label_comment_add", :sort => 1130, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "news", :action => "destroy_comment", :description => "label_comment_delete", :sort => 1133, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'add_comment']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'destroy_comment']).destroy
+ end
+end
--- /dev/null
+class CreateQueries < ActiveRecord::Migration
+ def self.up
+ create_table :queries, :force => true do |t|
+ t.column "project_id", :integer
+ t.column "name", :string, :default => "", :null => false
+ t.column "filters", :text
+ t.column "user_id", :integer, :default => 0, :null => false
+ t.column "is_public", :boolean, :default => false, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :queries
+ end
+end
--- /dev/null
+class AddQueriesPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "add_query", :description => "button_create", :sort => 600, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'add_query']).destroy
+ end
+end
--- /dev/null
+class CreateRepositories < ActiveRecord::Migration
+ def self.up
+ create_table :repositories, :force => true do |t|
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "url", :string, :default => "", :null => false
+ end
+ end
+
+ def self.down
+ drop_table :repositories
+ end
+end
--- /dev/null
+class AddRepositoriesPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "repositories", :action => "show", :description => "button_view", :sort => 1450, :is_public => true
+ Permission.create :controller => "repositories", :action => "browse", :description => "label_browse", :sort => 1460, :is_public => true
+ Permission.create :controller => "repositories", :action => "entry", :description => "entry", :sort => 1462, :is_public => true
+ Permission.create :controller => "repositories", :action => "revisions", :description => "label_view_revisions", :sort => 1470, :is_public => true
+ Permission.create :controller => "repositories", :action => "revision", :description => "label_view_revisions", :sort => 1472, :is_public => true
+ Permission.create :controller => "repositories", :action => "diff", :description => "diff", :sort => 1480, :is_public => true
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'show']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'browse']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'entry']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revisions']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revision']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'diff']).destroy
+ end
+end
--- /dev/null
+class CreateSettings < ActiveRecord::Migration
+ def self.up
+ create_table :settings, :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "value", :text
+ end
+ end
+
+ def self.down
+ drop_table :settings
+ end
+end
--- /dev/null
+class SetDocAndFilesNotifications < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.find_by_controller_and_action("projects", "add_file").update_attribute(:mail_option, true)
+ Permission.find_by_controller_and_action("projects", "add_document").update_attribute(:mail_option, true)
+ Permission.find_by_controller_and_action("documents", "add_attachment").update_attribute(:mail_option, true)
+ Permission.find_by_controller_and_action("issues", "add_attachment").update_attribute(:mail_option, true)
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action("projects", "add_file").update_attribute(:mail_option, false)
+ Permission.find_by_controller_and_action("projects", "add_document").update_attribute(:mail_option, false)
+ Permission.find_by_controller_and_action("documents", "add_attachment").update_attribute(:mail_option, false)
+ Permission.find_by_controller_and_action("issues", "add_attachment").update_attribute(:mail_option, false)
+ end
+end
--- /dev/null
+class AddIssueStatusPosition < ActiveRecord::Migration
+ def self.up
+ add_column :issue_statuses, :position, :integer, :default => 1
+ IssueStatus.find(:all).each_with_index {|status, i| status.update_attribute(:position, i+1)}
+ end
+
+ def self.down
+ remove_column :issue_statuses, :position
+ end
+end
--- /dev/null
+class AddRolePosition < ActiveRecord::Migration
+ def self.up
+ add_column :roles, :position, :integer, :default => 1
+ Role.find(:all).each_with_index {|role, i| role.update_attribute(:position, i+1)}
+ end
+
+ def self.down
+ remove_column :roles, :position
+ end
+end
--- /dev/null
+class AddTrackerPosition < ActiveRecord::Migration
+ def self.up
+ add_column :trackers, :position, :integer, :default => 1
+ Tracker.find(:all).each_with_index {|tracker, i| tracker.update_attribute(:position, i+1)}
+ end
+
+ def self.down
+ remove_column :trackers, :position
+ end
+end
--- /dev/null
+class SerializePossiblesValues < ActiveRecord::Migration
+ def self.up
+ CustomField.find(:all).each do |field|
+ if field.possible_values and field.possible_values.is_a? String
+ field.possible_values = field.possible_values.split('|')
+ field.save
+ end
+ end
+ end
+
+ def self.down
+ end
+end
--- /dev/null
+class AddTrackerIsInRoadmap < ActiveRecord::Migration
+ def self.up
+ add_column :trackers, :is_in_roadmap, :boolean, :default => true, :null => false
+ end
+
+ def self.down
+ remove_column :trackers, :is_in_roadmap
+ end
+end
--- /dev/null
+class AddRoadmapPermission < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "roadmap", :description => "label_roadmap", :sort => 107, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'roadmap']).destroy
+ end
+end
--- /dev/null
+class AddSearchPermission < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "search", :description => "label_search", :sort => 130, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('projects', 'search').destroy
+ end
+end
--- /dev/null
+class AddRepositoryLoginAndPassword < ActiveRecord::Migration
+ def self.up
+ add_column :repositories, :login, :string, :limit => 60, :default => ""
+ add_column :repositories, :password, :string, :limit => 60, :default => ""
+ end
+
+ def self.down
+ remove_column :repositories, :login
+ remove_column :repositories, :password
+ end
+end
--- /dev/null
+class CreateWikis < ActiveRecord::Migration
+ def self.up
+ create_table :wikis do |t|
+ t.column :project_id, :integer, :null => false
+ t.column :start_page, :string, :limit => 255, :null => false
+ t.column :status, :integer, :default => 1, :null => false
+ end
+ add_index :wikis, :project_id, :name => :wikis_project_id
+ end
+
+ def self.down
+ drop_table :wikis
+ end
+end
--- /dev/null
+class CreateWikiPages < ActiveRecord::Migration
+ def self.up
+ create_table :wiki_pages do |t|
+ t.column :wiki_id, :integer, :null => false
+ t.column :title, :string, :limit => 255, :null => false
+ t.column :created_on, :datetime, :null => false
+ end
+ add_index :wiki_pages, [:wiki_id, :title], :name => :wiki_pages_wiki_id_title
+ end
+
+ def self.down
+ drop_table :wiki_pages
+ end
+end
--- /dev/null
+class CreateWikiContents < ActiveRecord::Migration
+ def self.up
+ create_table :wiki_contents do |t|
+ t.column :page_id, :integer, :null => false
+ t.column :author_id, :integer
+ t.column :text, :text
+ t.column :comments, :string, :limit => 255, :default => ""
+ t.column :updated_on, :datetime, :null => false
+ t.column :version, :integer, :null => false
+ end
+ add_index :wiki_contents, :page_id, :name => :wiki_contents_page_id
+
+ create_table :wiki_content_versions do |t|
+ t.column :wiki_content_id, :integer, :null => false
+ t.column :page_id, :integer, :null => false
+ t.column :author_id, :integer
+ t.column :data, :binary
+ t.column :compression, :string, :limit => 6, :default => ""
+ t.column :comments, :string, :limit => 255, :default => ""
+ t.column :updated_on, :datetime, :null => false
+ t.column :version, :integer, :null => false
+ end
+ add_index :wiki_content_versions, :wiki_content_id, :name => :wiki_content_versions_wcid
+ end
+
+ def self.down
+ drop_table :wiki_contents
+ drop_table :wiki_content_versions
+ end
+end
--- /dev/null
+class AddProjectsFeedsPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "projects", :action => "feeds", :description => "label_feed_plural", :sort => 132, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('projects', 'feeds').destroy
+ end
+end
--- /dev/null
+class AddRepositoryRootUrl < ActiveRecord::Migration
+ def self.up
+ add_column :repositories, :root_url, :string, :limit => 255, :default => ""
+ end
+
+ def self.down
+ remove_column :repositories, :root_url
+ end
+end
--- /dev/null
+class CreateTimeEntries < ActiveRecord::Migration
+ def self.up
+ create_table :time_entries do |t|
+ t.column :project_id, :integer, :null => false
+ t.column :user_id, :integer, :null => false
+ t.column :issue_id, :integer
+ t.column :hours, :float, :null => false
+ t.column :comments, :string, :limit => 255
+ t.column :activity_id, :integer, :null => false
+ t.column :spent_on, :date, :null => false
+ t.column :tyear, :integer, :null => false
+ t.column :tmonth, :integer, :null => false
+ t.column :tweek, :integer, :null => false
+ t.column :created_on, :datetime, :null => false
+ t.column :updated_on, :datetime, :null => false
+ end
+ add_index :time_entries, [:project_id], :name => :time_entries_project_id
+ add_index :time_entries, [:issue_id], :name => :time_entries_issue_id
+ end
+
+ def self.down
+ drop_table :time_entries
+ end
+end
--- /dev/null
+class AddTimelogPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "timelog", :action => "edit", :description => "button_log_time", :sort => 1520, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('timelog', 'edit').destroy
+ end
+end
--- /dev/null
+class CreateChangesets < ActiveRecord::Migration
+ def self.up
+ create_table :changesets do |t|
+ t.column :repository_id, :integer, :null => false
+ t.column :revision, :integer, :null => false
+ t.column :committer, :string, :limit => 30
+ t.column :committed_on, :datetime, :null => false
+ t.column :comments, :text
+ end
+ add_index :changesets, [:repository_id, :revision], :unique => true, :name => :changesets_repos_rev
+ end
+
+ def self.down
+ drop_table :changesets
+ end
+end
--- /dev/null
+class CreateChanges < ActiveRecord::Migration
+ def self.up
+ create_table :changes do |t|
+ t.column :changeset_id, :integer, :null => false
+ t.column :action, :string, :limit => 1, :default => "", :null => false
+ t.column :path, :string, :default => "", :null => false
+ t.column :from_path, :string
+ t.column :from_revision, :integer
+ end
+ add_index :changes, [:changeset_id], :name => :changesets_changeset_id
+ end
+
+ def self.down
+ drop_table :changes
+ end
+end
--- /dev/null
+class AddChangesetCommitDate < ActiveRecord::Migration
+ def self.up
+ add_column :changesets, :commit_date, :date
+ Changeset.update_all "commit_date = committed_on"
+ end
+
+ def self.down
+ remove_column :changesets, :commit_date
+ end
+end
--- /dev/null
+class AddProjectIdentifier < ActiveRecord::Migration
+ def self.up
+ add_column :projects, :identifier, :string, :limit => 20
+ end
+
+ def self.down
+ remove_column :projects, :identifier
+ end
+end
--- /dev/null
+class AddCustomFieldIsFilter < ActiveRecord::Migration
+ def self.up
+ add_column :custom_fields, :is_filter, :boolean, :null => false, :default => false
+ end
+
+ def self.down
+ remove_column :custom_fields, :is_filter
+ end
+end
--- /dev/null
+class CreateWatchers < ActiveRecord::Migration
+ def self.up
+ create_table :watchers do |t|
+ t.column :watchable_type, :string, :default => "", :null => false
+ t.column :watchable_id, :integer, :default => 0, :null => false
+ t.column :user_id, :integer
+ end
+ end
+
+ def self.down
+ drop_table :watchers
+ end
+end
--- /dev/null
+class CreateChangesetsIssues < ActiveRecord::Migration
+ def self.up
+ create_table :changesets_issues, :id => false do |t|
+ t.column :changeset_id, :integer, :null => false
+ t.column :issue_id, :integer, :null => false
+ end
+ add_index :changesets_issues, [:changeset_id, :issue_id], :unique => true, :name => :changesets_issues_ids
+ end
+
+ def self.down
+ drop_table :changesets_issues
+ end
+end
--- /dev/null
+class RenameCommentToComments < ActiveRecord::Migration
+ def self.up
+ rename_column(:comments, :comment, :comments) if ActiveRecord::Base.connection.columns(Comment.table_name).detect{|c| c.name == "comment"}
+ rename_column(:wiki_contents, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.table_name).detect{|c| c.name == "comment"}
+ rename_column(:wiki_content_versions, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.versioned_table_name).detect{|c| c.name == "comment"}
+ rename_column(:time_entries, :comment, :comments) if ActiveRecord::Base.connection.columns(TimeEntry.table_name).detect{|c| c.name == "comment"}
+ rename_column(:changesets, :comment, :comments) if ActiveRecord::Base.connection.columns(Changeset.table_name).detect{|c| c.name == "comment"}
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class CreateIssueRelations < ActiveRecord::Migration
+ def self.up
+ create_table :issue_relations do |t|
+ t.column :issue_from_id, :integer, :null => false
+ t.column :issue_to_id, :integer, :null => false
+ t.column :relation_type, :string, :default => "", :null => false
+ t.column :delay, :integer
+ end
+ end
+
+ def self.down
+ drop_table :issue_relations
+ end
+end
--- /dev/null
+class AddRelationsPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "issue_relations", :action => "new", :description => "label_relation_new", :sort => 1080, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "issue_relations", :action => "destroy", :description => "label_relation_delete", :sort => 1085, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action("issue_relations", "new").destroy
+ Permission.find_by_controller_and_action("issue_relations", "destroy").destroy
+ end
+end
--- /dev/null
+class SetLanguageLengthToFive < ActiveRecord::Migration
+ def self.up
+ change_column :users, :language, :string, :limit => 5, :default => ""
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class CreateBoards < ActiveRecord::Migration
+ def self.up
+ create_table :boards do |t|
+ t.column :project_id, :integer, :null => false
+ t.column :name, :string, :default => "", :null => false
+ t.column :description, :string
+ t.column :position, :integer, :default => 1
+ t.column :topics_count, :integer, :default => 0, :null => false
+ t.column :messages_count, :integer, :default => 0, :null => false
+ t.column :last_message_id, :integer
+ end
+ add_index :boards, [:project_id], :name => :boards_project_id
+ end
+
+ def self.down
+ drop_table :boards
+ end
+end
--- /dev/null
+class CreateMessages < ActiveRecord::Migration
+ def self.up
+ create_table :messages do |t|
+ t.column :board_id, :integer, :null => false
+ t.column :parent_id, :integer
+ t.column :subject, :string, :default => "", :null => false
+ t.column :content, :text
+ t.column :author_id, :integer
+ t.column :replies_count, :integer, :default => 0, :null => false
+ t.column :last_reply_id, :integer
+ t.column :created_on, :datetime, :null => false
+ t.column :updated_on, :datetime, :null => false
+ end
+ add_index :messages, [:board_id], :name => :messages_board_id
+ add_index :messages, [:parent_id], :name => :messages_parent_id
+ end
+
+ def self.down
+ drop_table :messages
+ end
+end
--- /dev/null
+class AddBoardsPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => "boards", :action => "new", :description => "button_add", :sort => 2000, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "boards", :action => "edit", :description => "button_edit", :sort => 2005, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "boards", :action => "destroy", :description => "button_delete", :sort => 2010, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action("boards", "new").destroy
+ Permission.find_by_controller_and_action("boards", "edit").destroy
+ Permission.find_by_controller_and_action("boards", "destroy").destroy
+ end
+end
--- /dev/null
+class AllowNullVersionEffectiveDate < ActiveRecord::Migration
+ def self.up
+ change_column :versions, :effective_date, :date, :default => nil, :null => true
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class AddWikiDestroyPagePermission < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => 'wiki', :action => 'destroy', :description => 'button_delete', :sort => 1740, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('wiki', 'destroy').destroy
+ end
+end
--- /dev/null
+class AddWikiAttachmentsPermissions < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => 'wiki', :action => 'add_attachment', :description => 'label_attachment_new', :sort => 1750, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => 'wiki', :action => 'destroy_attachment', :description => 'label_attachment_delete', :sort => 1755, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('wiki', 'add_attachment').destroy
+ Permission.find_by_controller_and_action('wiki', 'destroy_attachment').destroy
+ end
+end
--- /dev/null
+class AddProjectStatus < ActiveRecord::Migration
+ def self.up
+ add_column :projects, :status, :integer, :default => 1, :null => false
+ end
+
+ def self.down
+ remove_column :projects, :status
+ end
+end
--- /dev/null
+class AddChangesRevision < ActiveRecord::Migration
+ def self.up
+ add_column :changes, :revision, :string
+ end
+
+ def self.down
+ remove_column :changes, :revision
+ end
+end
--- /dev/null
+class AddChangesBranch < ActiveRecord::Migration
+ def self.up
+ add_column :changes, :branch, :string
+ end
+
+ def self.down
+ remove_column :changes, :branch
+ end
+end
--- /dev/null
+class AddChangesetsScmid < ActiveRecord::Migration
+ def self.up
+ add_column :changesets, :scmid, :string
+ end
+
+ def self.down
+ remove_column :changesets, :scmid
+ end
+end
--- /dev/null
+class AddRepositoriesType < ActiveRecord::Migration
+ def self.up
+ add_column :repositories, :type, :string
+ # Set class name for existing SVN repositories
+ Repository.update_all "type = 'Subversion'"
+ end
+
+ def self.down
+ remove_column :repositories, :type
+ end
+end
--- /dev/null
+class AddRepositoriesChangesPermission < ActiveRecord::Migration
+ # model removed
+ class Permission < ActiveRecord::Base; end
+
+ def self.up
+ Permission.create :controller => 'repositories', :action => 'changes', :description => 'label_change_plural', :sort => 1475, :is_public => true, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action('repositories', 'changes').destroy
+ end
+end
--- /dev/null
+class AddVersionsWikiPageTitle < ActiveRecord::Migration
+ def self.up
+ add_column :versions, :wiki_page_title, :string
+ end
+
+ def self.down
+ remove_column :versions, :wiki_page_title
+ end
+end
--- /dev/null
+class AddIssueCategoriesAssignedToId < ActiveRecord::Migration
+ def self.up
+ add_column :issue_categories, :assigned_to_id, :integer
+ end
+
+ def self.down
+ remove_column :issue_categories, :assigned_to_id
+ end
+end
--- /dev/null
+class AddRolesAssignable < ActiveRecord::Migration
+ def self.up
+ add_column :roles, :assignable, :boolean, :default => true
+ end
+
+ def self.down
+ remove_column :roles, :assignable
+ end
+end
--- /dev/null
+class ChangeChangesetsCommitterLimit < ActiveRecord::Migration
+ def self.up
+ change_column :changesets, :committer, :string, :limit => nil
+ end
+
+ def self.down
+ change_column :changesets, :committer, :string, :limit => 30
+ end
+end
--- /dev/null
+class AddRolesBuiltin < ActiveRecord::Migration
+ def self.up
+ add_column :roles, :builtin, :integer, :default => 0, :null => false
+ end
+
+ def self.down
+ remove_column :roles, :builtin
+ end
+end
--- /dev/null
+class InsertBuiltinRoles < ActiveRecord::Migration
+ def self.up
+ nonmember = Role.new(:name => 'Non member', :position => 0)
+ nonmember.builtin = Role::BUILTIN_NON_MEMBER
+ nonmember.save
+
+ anonymous = Role.new(:name => 'Anonymous', :position => 0)
+ anonymous.builtin = Role::BUILTIN_ANONYMOUS
+ anonymous.save
+ end
+
+ def self.down
+ Role.destroy_all 'builtin <> 0'
+ end
+end
--- /dev/null
+class AddRolesPermissions < ActiveRecord::Migration
+ def self.up
+ add_column :roles, :permissions, :text
+ end
+
+ def self.down
+ remove_column :roles, :permissions
+ end
+end
--- /dev/null
+class DropPermissions < ActiveRecord::Migration
+ def self.up
+ drop_table :permissions
+ drop_table :permissions_roles
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class AddSettingsUpdatedOn < ActiveRecord::Migration
+ def self.up
+ add_column :settings, :updated_on, :timestamp
+ # set updated_on
+ Setting.find(:all).each(&:save)
+ end
+
+ def self.down
+ remove_column :settings, :updated_on
+ end
+end
--- /dev/null
+class AddCustomValueCustomizedIndex < ActiveRecord::Migration
+ def self.up
+ add_index :custom_values, [:customized_type, :customized_id], :name => :custom_values_customized
+ end
+
+ def self.down
+ remove_index :custom_values, :name => :custom_values_customized
+ end
+end
--- /dev/null
+class CreateWikiRedirects < ActiveRecord::Migration
+ def self.up
+ create_table :wiki_redirects do |t|
+ t.column :wiki_id, :integer, :null => false
+ t.column :title, :string
+ t.column :redirects_to, :string
+ t.column :created_on, :datetime, :null => false
+ end
+ add_index :wiki_redirects, [:wiki_id, :title], :name => :wiki_redirects_wiki_id_title
+ end
+
+ def self.down
+ drop_table :wiki_redirects
+ end
+end
--- /dev/null
+class CreateEnabledModules < ActiveRecord::Migration
+ def self.up
+ create_table :enabled_modules do |t|
+ t.column :project_id, :integer
+ t.column :name, :string, :null => false
+ end
+ add_index :enabled_modules, [:project_id], :name => :enabled_modules_project_id
+
+ # Enable all modules for existing projects
+ Project.find(:all).each do |project|
+ project.enabled_module_names = Redmine::AccessControl.available_project_modules
+ end
+ end
+
+ def self.down
+ drop_table :enabled_modules
+ end
+end
--- /dev/null
+class AddIssuesEstimatedHours < ActiveRecord::Migration
+ def self.up
+ add_column :issues, :estimated_hours, :float
+ end
+
+ def self.down
+ remove_column :issues, :estimated_hours
+ end
+end
--- /dev/null
+class ChangeAttachmentsContentTypeLimit < ActiveRecord::Migration
+ def self.up
+ change_column :attachments, :content_type, :string, :limit => nil
+ end
+
+ def self.down
+ change_column :attachments, :content_type, :string, :limit => 60
+ end
+end
--- /dev/null
+class AddQueriesColumnNames < ActiveRecord::Migration
+ def self.up
+ add_column :queries, :column_names, :text
+ end
+
+ def self.down
+ remove_column :queries, :column_names
+ end
+end
--- /dev/null
+class AddEnumerationsPosition < ActiveRecord::Migration
+ def self.up
+ add_column(:enumerations, :position, :integer, :default => 1) unless Enumeration.column_names.include?('position')
+ Enumeration.find(:all).group_by(&:opt).each do |opt, enums|
+ enums.each_with_index do |enum, i|
+ # do not call model callbacks
+ Enumeration.update_all "position = #{i+1}", {:id => enum.id}
+ end
+ end
+ end
+
+ def self.down
+ remove_column :enumerations, :position
+ end
+end
--- /dev/null
+class AddEnumerationsIsDefault < ActiveRecord::Migration
+ def self.up
+ add_column :enumerations, :is_default, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :enumerations, :is_default
+ end
+end
--- /dev/null
+class AddAuthSourcesTls < ActiveRecord::Migration
+ def self.up
+ add_column :auth_sources, :tls, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :auth_sources, :tls
+ end
+end
--- /dev/null
+class AddMembersMailNotification < ActiveRecord::Migration
+ def self.up
+ add_column :members, :mail_notification, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :members, :mail_notification
+ end
+end
--- /dev/null
+class AllowNullPosition < ActiveRecord::Migration
+ def self.up
+ # removes the 'not null' constraint on position fields
+ change_column :issue_statuses, :position, :integer, :default => 1, :null => true
+ change_column :roles, :position, :integer, :default => 1, :null => true
+ change_column :trackers, :position, :integer, :default => 1, :null => true
+ change_column :boards, :position, :integer, :default => 1, :null => true
+ change_column :enumerations, :position, :integer, :default => 1, :null => true
+ end
+
+ def self.down
+ # nothing to do
+ end
+end
--- /dev/null
+class RemoveIssueStatusesHtmlColor < ActiveRecord::Migration
+ def self.up
+ remove_column :issue_statuses, :html_color
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class AddCustomFieldsPosition < ActiveRecord::Migration
+ def self.up
+ add_column(:custom_fields, :position, :integer, :default => 1)
+ CustomField.find(:all).group_by(&:type).each do |t, fields|
+ fields.each_with_index do |field, i|
+ # do not call model callbacks
+ CustomField.update_all "position = #{i+1}", {:id => field.id}
+ end
+ end
+ end
+
+ def self.down
+ remove_column :custom_fields, :position
+ end
+end
--- /dev/null
+class AddUserPreferencesTimeZone < ActiveRecord::Migration
+ def self.up
+ add_column :user_preferences, :time_zone, :string
+ end
+
+ def self.down
+ remove_column :user_preferences, :time_zone
+ end
+end
--- /dev/null
+class AddUsersType < ActiveRecord::Migration
+ def self.up
+ add_column :users, :type, :string
+ User.update_all "type = 'User'"
+ end
+
+ def self.down
+ remove_column :users, :type
+ end
+end
--- /dev/null
+class CreateProjectsTrackers < ActiveRecord::Migration
+ def self.up
+ create_table :projects_trackers, :id => false do |t|
+ t.column :project_id, :integer, :default => 0, :null => false
+ t.column :tracker_id, :integer, :default => 0, :null => false
+ end
+ add_index :projects_trackers, :project_id, :name => :projects_trackers_project_id
+
+ # Associates all trackers to all projects (as it was before)
+ tracker_ids = Tracker.find(:all).collect(&:id)
+ Project.find(:all).each do |project|
+ project.tracker_ids = tracker_ids
+ end
+ end
+
+ def self.down
+ drop_table :projects_trackers
+ end
+end
--- /dev/null
+class AddMessagesLocked < ActiveRecord::Migration
+ def self.up
+ add_column :messages, :locked, :boolean, :default => false
+ end
+
+ def self.down
+ remove_column :messages, :locked
+ end
+end
--- /dev/null
+class AddMessagesSticky < ActiveRecord::Migration
+ def self.up
+ add_column :messages, :sticky, :integer, :default => 0
+ end
+
+ def self.down
+ remove_column :messages, :sticky
+ end
+end
--- /dev/null
+class ChangeAuthSourcesAccountLimit < ActiveRecord::Migration
+ def self.up
+ change_column :auth_sources, :account, :string, :limit => nil
+ end
+
+ def self.down
+ change_column :auth_sources, :account, :string, :limit => 60
+ end
+end
--- /dev/null
+class AddRoleTrackerOldStatusIndexToWorkflows < ActiveRecord::Migration
+ def self.up
+ add_index :workflows, [:role_id, :tracker_id, :old_status_id], :name => :wkfs_role_tracker_old_status
+ end
+
+ def self.down
+ remove_index(:workflows, :name => :wkfs_role_tracker_old_status); rescue
+ end
+end
--- /dev/null
+class AddCustomFieldsSearchable < ActiveRecord::Migration
+ def self.up
+ add_column :custom_fields, :searchable, :boolean, :default => false
+ end
+
+ def self.down
+ remove_column :custom_fields, :searchable
+ end
+end
--- /dev/null
+class ChangeProjectsDescriptionToText < ActiveRecord::Migration
+ def self.up
+ change_column :projects, :description, :text, :null => true, :default => nil
+ end
+
+ def self.down
+ end
+end
--- /dev/null
+class AddCustomFieldsDefaultValue < ActiveRecord::Migration
+ def self.up
+ add_column :custom_fields, :default_value, :text
+ end
+
+ def self.down
+ remove_column :custom_fields, :default_value
+ end
+end
--- /dev/null
+class AddAttachmentsDescription < ActiveRecord::Migration
+ def self.up
+ add_column :attachments, :description, :string
+ end
+
+ def self.down
+ remove_column :attachments, :description
+ end
+end
--- /dev/null
+class ChangeVersionsNameLimit < ActiveRecord::Migration
+ def self.up
+ change_column :versions, :name, :string, :limit => nil
+ end
+
+ def self.down
+ change_column :versions, :name, :string, :limit => 30
+ end
+end
--- /dev/null
+class ChangeChangesetsRevisionToString < ActiveRecord::Migration
+ def self.up
+ change_column :changesets, :revision, :string, :null => false
+ end
+
+ def self.down
+ change_column :changesets, :revision, :integer, :null => false
+ end
+end
--- /dev/null
+class ChangeChangesFromRevisionToString < ActiveRecord::Migration
+ def self.up
+ change_column :changes, :from_revision, :string
+ end
+
+ def self.down
+ change_column :changes, :from_revision, :integer
+ end
+end
--- /dev/null
+class AddWikiPagesProtected < ActiveRecord::Migration
+ def self.up
+ add_column :wiki_pages, :protected, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :wiki_pages, :protected
+ end
+end
--- /dev/null
+class ChangeProjectsHomepageLimit < ActiveRecord::Migration
+ def self.up
+ change_column :projects, :homepage, :string, :limit => nil, :default => ''
+ end
+
+ def self.down
+ change_column :projects, :homepage, :string, :limit => 60, :default => ''
+ end
+end
--- /dev/null
+class AddWikiPagesParentId < ActiveRecord::Migration
+ def self.up
+ add_column :wiki_pages, :parent_id, :integer, :default => nil
+ end
+
+ def self.down
+ remove_column :wiki_pages, :parent_id
+ end
+end
--- /dev/null
+class AddCommitAccessPermission < ActiveRecord::Migration
+
+ def self.up
+ Role.find(:all).select { |r| not r.builtin? }.each do |r|
+ r.add_permission!(:commit_access)
+ end
+ end
+
+ def self.down
+ Role.find(:all).select { |r| not r.builtin? }.each do |r|
+ r.remove_permission!(:commit_access)
+ end
+ end
+end
--- /dev/null
+class AddViewWikiEditsPermission < ActiveRecord::Migration
+ def self.up
+ Role.find(:all).each do |r|
+ r.add_permission!(:view_wiki_edits) if r.has_permission?(:view_wiki_pages)
+ end
+ end
+
+ def self.down
+ Role.find(:all).each do |r|
+ r.remove_permission!(:view_wiki_edits)
+ end
+ end
+end
--- /dev/null
+class SetTopicAuthorsAsWatchers < ActiveRecord::Migration
+ def self.up
+ # Sets active users who created/replied a topic as watchers of the topic
+ # so that the new watch functionality at topic level doesn't affect notifications behaviour
+ Message.connection.execute("INSERT INTO #{Watcher.table_name} (watchable_type, watchable_id, user_id)" +
+ " SELECT DISTINCT 'Message', COALESCE(m.parent_id, m.id), m.author_id" +
+ " FROM #{Message.table_name} m, #{User.table_name} u" +
+ " WHERE m.author_id = u.id AND u.status = 1")
+ end
+
+ def self.down
+ # Removes all message watchers
+ Watcher.delete_all("watchable_type = 'Message'")
+ end
+end
--- /dev/null
+class AddDeleteWikiPagesAttachmentsPermission < ActiveRecord::Migration
+ def self.up
+ Role.find(:all).each do |r|
+ r.add_permission!(:delete_wiki_pages_attachments) if r.has_permission?(:edit_wiki_pages)
+ end
+ end
+
+ def self.down
+ Role.find(:all).each do |r|
+ r.remove_permission!(:delete_wiki_pages_attachments)
+ end
+ end
+end
--- /dev/null
+class AddChangesetsUserId < ActiveRecord::Migration
+ def self.up
+ add_column :changesets, :user_id, :integer, :default => nil
+ end
+
+ def self.down
+ remove_column :changesets, :user_id
+ end
+end
--- /dev/null
+class PopulateChangesetsUserId < ActiveRecord::Migration
+ def self.up
+ committers = Changeset.connection.select_values("SELECT DISTINCT committer FROM #{Changeset.table_name}")
+ committers.each do |committer|
+ next if committer.blank?
+ if committer.strip =~ /^([^<]+)(<(.*)>)?$/
+ username, email = $1.strip, $3
+ u = User.find_by_login(username)
+ u ||= User.find_by_mail(email) unless email.blank?
+ Changeset.update_all("user_id = #{u.id}", ["committer = ?", committer]) unless u.nil?
+ end
+ end
+ end
+
+ def self.down
+ Changeset.update_all('user_id = NULL')
+ end
+end
--- /dev/null
+class AddCustomFieldsEditable < ActiveRecord::Migration
+ def self.up
+ add_column :custom_fields, :editable, :boolean, :default => true
+ end
+
+ def self.down
+ remove_column :custom_fields, :editable
+ end
+end
--- /dev/null
+class SetCustomFieldsEditable < ActiveRecord::Migration
+ def self.up
+ UserCustomField.update_all("editable = #{CustomField.connection.quoted_false}")
+ end
+
+ def self.down
+ UserCustomField.update_all("editable = #{CustomField.connection.quoted_true}")
+ end
+end
--- /dev/null
+class AddProjectsLftAndRgt < ActiveRecord::Migration
+ def self.up
+ add_column :projects, :lft, :integer
+ add_column :projects, :rgt, :integer
+ end
+
+ def self.down
+ remove_column :projects, :lft
+ remove_column :projects, :rgt
+ end
+end
--- /dev/null
+class BuildProjectsTree < ActiveRecord::Migration
+ def self.up
+ Project.rebuild!
+ end
+
+ def self.down
+ end
+end
--- /dev/null
+class RemoveProjectsProjectsCount < ActiveRecord::Migration
+ def self.up
+ remove_column :projects, :projects_count
+ end
+
+ def self.down
+ add_column :projects, :projects_count, :integer, :default => 0
+ end
+end
--- /dev/null
+class AddOpenIdAuthenticationTables < ActiveRecord::Migration
+ def self.up
+ create_table :open_id_authentication_associations, :force => true do |t|
+ t.integer :issued, :lifetime
+ t.string :handle, :assoc_type
+ t.binary :server_url, :secret
+ end
+
+ create_table :open_id_authentication_nonces, :force => true do |t|
+ t.integer :timestamp, :null => false
+ t.string :server_url, :null => true
+ t.string :salt, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :open_id_authentication_associations
+ drop_table :open_id_authentication_nonces
+ end
+end
--- /dev/null
+class AddIdentityUrlToUsers < ActiveRecord::Migration
+ def self.up
+ add_column :users, :identity_url, :string
+ end
+
+ def self.down
+ remove_column :users, :identity_url
+ end
+end
--- /dev/null
+class AddWatchersUserIdTypeIndex < ActiveRecord::Migration
+ def self.up
+ add_index :watchers, [:user_id, :watchable_type], :name => :watchers_user_id_type
+ end
+
+ def self.down
+ remove_index :watchers, :name => :watchers_user_id_type
+ end
+end
--- /dev/null
+class AddQueriesSortCriteria < ActiveRecord::Migration
+ def self.up
+ add_column :queries, :sort_criteria, :text
+ end
+
+ def self.down
+ remove_column :queries, :sort_criteria
+ end
+end
--- /dev/null
+class AddProjectsTrackersUniqueIndex < ActiveRecord::Migration
+ def self.up
+ remove_duplicates
+ add_index :projects_trackers, [:project_id, :tracker_id], :name => :projects_trackers_unique, :unique => true
+ end
+
+ def self.down
+ remove_index :projects_trackers, :name => :projects_trackers_unique
+ end
+
+ # Removes duplicates in projects_trackers table
+ def self.remove_duplicates
+ Project.find(:all).each do |project|
+ ids = project.trackers.collect(&:id)
+ unless ids == ids.uniq
+ project.trackers.clear
+ project.tracker_ids = ids.uniq
+ end
+ end
+ end
+end
--- /dev/null
+class ExtendSettingsName < ActiveRecord::Migration
+ def self.up
+ change_column :settings, :name, :string, :limit => 255, :default => '', :null => false
+ end
+
+ def self.down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
--- /dev/null
+class AddTypeToEnumerations < ActiveRecord::Migration
+ def self.up
+ add_column :enumerations, :type, :string
+ end
+
+ def self.down
+ remove_column :enumerations, :type
+ end
+end
--- /dev/null
+class UpdateEnumerationsToSti < ActiveRecord::Migration
+ def self.up
+ Enumeration.update_all("type = 'IssuePriority'", "opt = 'IPRI'")
+ Enumeration.update_all("type = 'DocumentCategory'", "opt = 'DCAT'")
+ Enumeration.update_all("type = 'TimeEntryActivity'", "opt = 'ACTI'")
+ end
+
+ def self.down
+ # no-op
+ end
+end
--- /dev/null
+class AddActiveFieldToEnumerations < ActiveRecord::Migration
+ def self.up
+ add_column :enumerations, :active, :boolean, :default => true, :null => false
+ end
+
+ def self.down
+ remove_column :enumerations, :active
+ end
+end
--- /dev/null
+class AddProjectToEnumerations < ActiveRecord::Migration
+ def self.up
+ add_column :enumerations, :project_id, :integer, :null => true, :default => nil
+ add_index :enumerations, :project_id
+ end
+
+ def self.down
+ remove_index :enumerations, :project_id
+ remove_column :enumerations, :project_id
+ end
+end
--- /dev/null
+class AddParentIdToEnumerations < ActiveRecord::Migration
+ def self.up
+ add_column :enumerations, :parent_id, :integer, :null => true, :default => nil
+ end
+
+ def self.down
+ remove_column :enumerations, :parent_id
+ end
+end
--- /dev/null
+class AddQueriesGroupBy < ActiveRecord::Migration
+ def self.up
+ add_column :queries, :group_by, :string
+ end
+
+ def self.down
+ remove_column :queries, :group_by
+ end
+end
--- /dev/null
+class CreateMemberRoles < ActiveRecord::Migration
+ def self.up
+ create_table :member_roles do |t|
+ t.column :member_id, :integer, :null => false
+ t.column :role_id, :integer, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :member_roles
+ end
+end
--- /dev/null
+class PopulateMemberRoles < ActiveRecord::Migration
+ def self.up
+ MemberRole.delete_all
+ Member.find(:all).each do |member|
+ MemberRole.create!(:member_id => member.id, :role_id => member.role_id)
+ end
+ end
+
+ def self.down
+ MemberRole.delete_all
+ end
+end
--- /dev/null
+class DropMembersRoleId < ActiveRecord::Migration
+ def self.up
+ remove_column :members, :role_id
+ end
+
+ def self.down
+ raise IrreversibleMigration
+ end
+end
--- /dev/null
+class FixMessagesStickyNull < ActiveRecord::Migration
+ def self.up
+ Message.update_all('sticky = 0', 'sticky IS NULL')
+ end
+
+ def self.down
+ # nothing to do
+ end
+end
--- /dev/null
+class PopulateUsersType < ActiveRecord::Migration
+ def self.up
+ Principal.update_all("type = 'User'", "type IS NULL")
+ end
+
+ def self.down
+ end
+end
--- /dev/null
+class CreateGroupsUsers < ActiveRecord::Migration
+ def self.up
+ create_table :groups_users, :id => false do |t|
+ t.column :group_id, :integer, :null => false
+ t.column :user_id, :integer, :null => false
+ end
+ add_index :groups_users, [:group_id, :user_id], :unique => true, :name => :groups_users_ids
+ end
+
+ def self.down
+ drop_table :groups_users
+ end
+end
--- /dev/null
+class AddMemberRolesInheritedFrom < ActiveRecord::Migration
+ def self.up
+ add_column :member_roles, :inherited_from, :integer
+ end
+
+ def self.down
+ remove_column :member_roles, :inherited_from
+ end
+end
--- /dev/null
+class FixUsersCustomValues < ActiveRecord::Migration\r
+ def self.up\r
+ CustomValue.update_all("customized_type = 'Principal'", "customized_type = 'User'")\r
+ end\r
+\r
+ def self.down\r
+ CustomValue.update_all("customized_type = 'User'", "customized_type = 'Principal'")\r
+ end\r
+end\r
--- /dev/null
+class AddMissingIndexesToWorkflows < ActiveRecord::Migration
+ def self.up
+ add_index :workflows, :old_status_id
+ add_index :workflows, :role_id
+ add_index :workflows, :new_status_id
+ end
+
+ def self.down
+ remove_index :workflows, :old_status_id
+ remove_index :workflows, :role_id
+ remove_index :workflows, :new_status_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToCustomFieldsProjects < ActiveRecord::Migration
+ def self.up
+ add_index :custom_fields_projects, [:custom_field_id, :project_id]
+ end
+
+ def self.down
+ remove_index :custom_fields_projects, :column => [:custom_field_id, :project_id]
+ end
+end
--- /dev/null
+class AddMissingIndexesToMessages < ActiveRecord::Migration
+ def self.up
+ add_index :messages, :last_reply_id
+ add_index :messages, :author_id
+ end
+
+ def self.down
+ remove_index :messages, :last_reply_id
+ remove_index :messages, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToRepositories < ActiveRecord::Migration
+ def self.up
+ add_index :repositories, :project_id
+ end
+
+ def self.down
+ remove_index :repositories, :project_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToComments < ActiveRecord::Migration
+ def self.up
+ add_index :comments, [:commented_id, :commented_type]
+ add_index :comments, :author_id
+ end
+
+ def self.down
+ remove_index :comments, :column => [:commented_id, :commented_type]
+ remove_index :comments, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToEnumerations < ActiveRecord::Migration
+ def self.up
+ add_index :enumerations, [:id, :type]
+ end
+
+ def self.down
+ remove_index :enumerations, :column => [:id, :type]
+ end
+end
--- /dev/null
+class AddMissingIndexesToWikiPages < ActiveRecord::Migration
+ def self.up
+ add_index :wiki_pages, :wiki_id
+ add_index :wiki_pages, :parent_id
+ end
+
+ def self.down
+ remove_index :wiki_pages, :wiki_id
+ remove_index :wiki_pages, :parent_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToWatchers < ActiveRecord::Migration
+ def self.up
+ add_index :watchers, :user_id
+ add_index :watchers, [:watchable_id, :watchable_type]
+ end
+
+ def self.down
+ remove_index :watchers, :user_id
+ remove_index :watchers, :column => [:watchable_id, :watchable_type]
+ end
+end
--- /dev/null
+class AddMissingIndexesToAuthSources < ActiveRecord::Migration
+ def self.up
+ add_index :auth_sources, [:id, :type]
+ end
+
+ def self.down
+ remove_index :auth_sources, :column => [:id, :type]
+ end
+end
--- /dev/null
+class AddMissingIndexesToDocuments < ActiveRecord::Migration
+ def self.up
+ add_index :documents, :category_id
+ end
+
+ def self.down
+ remove_index :documents, :category_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToTokens < ActiveRecord::Migration
+ def self.up
+ add_index :tokens, :user_id
+ end
+
+ def self.down
+ remove_index :tokens, :user_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToChangesets < ActiveRecord::Migration
+ def self.up
+ add_index :changesets, :user_id
+ add_index :changesets, :repository_id
+ end
+
+ def self.down
+ remove_index :changesets, :user_id
+ remove_index :changesets, :repository_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToIssueCategories < ActiveRecord::Migration
+ def self.up
+ add_index :issue_categories, :assigned_to_id
+ end
+
+ def self.down
+ remove_index :issue_categories, :assigned_to_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToMemberRoles < ActiveRecord::Migration
+ def self.up
+ add_index :member_roles, :member_id
+ add_index :member_roles, :role_id
+ end
+
+ def self.down
+ remove_index :member_roles, :member_id
+ remove_index :member_roles, :role_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToBoards < ActiveRecord::Migration
+ def self.up
+ add_index :boards, :last_message_id
+ end
+
+ def self.down
+ remove_index :boards, :last_message_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToUserPreferences < ActiveRecord::Migration
+ def self.up
+ add_index :user_preferences, :user_id
+ end
+
+ def self.down
+ remove_index :user_preferences, :user_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToIssues < ActiveRecord::Migration
+ def self.up
+ add_index :issues, :status_id
+ add_index :issues, :category_id
+ add_index :issues, :assigned_to_id
+ add_index :issues, :fixed_version_id
+ add_index :issues, :tracker_id
+ add_index :issues, :priority_id
+ add_index :issues, :author_id
+ end
+
+ def self.down
+ remove_index :issues, :status_id
+ remove_index :issues, :category_id
+ remove_index :issues, :assigned_to_id
+ remove_index :issues, :fixed_version_id
+ remove_index :issues, :tracker_id
+ remove_index :issues, :priority_id
+ remove_index :issues, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToMembers < ActiveRecord::Migration
+ def self.up
+ add_index :members, :user_id
+ add_index :members, :project_id
+ end
+
+ def self.down
+ remove_index :members, :user_id
+ remove_index :members, :project_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToCustomFields < ActiveRecord::Migration
+ def self.up
+ add_index :custom_fields, [:id, :type]
+ end
+
+ def self.down
+ remove_index :custom_fields, :column => [:id, :type]
+ end
+end
--- /dev/null
+class AddMissingIndexesToQueries < ActiveRecord::Migration
+ def self.up
+ add_index :queries, :project_id
+ add_index :queries, :user_id
+ end
+
+ def self.down
+ remove_index :queries, :project_id
+ remove_index :queries, :user_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToTimeEntries < ActiveRecord::Migration
+ def self.up
+ add_index :time_entries, :activity_id
+ add_index :time_entries, :user_id
+ end
+
+ def self.down
+ remove_index :time_entries, :activity_id
+ remove_index :time_entries, :user_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToNews < ActiveRecord::Migration
+ def self.up
+ add_index :news, :author_id
+ end
+
+ def self.down
+ remove_index :news, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToUsers < ActiveRecord::Migration
+ def self.up
+ add_index :users, [:id, :type]
+ add_index :users, :auth_source_id
+ end
+
+ def self.down
+ remove_index :users, :column => [:id, :type]
+ remove_index :users, :auth_source_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToAttachments < ActiveRecord::Migration
+ def self.up
+ add_index :attachments, [:container_id, :container_type]
+ add_index :attachments, :author_id
+ end
+
+ def self.down
+ remove_index :attachments, :column => [:container_id, :container_type]
+ remove_index :attachments, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToWikiContents < ActiveRecord::Migration
+ def self.up
+ add_index :wiki_contents, :author_id
+ end
+
+ def self.down
+ remove_index :wiki_contents, :author_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToCustomValues < ActiveRecord::Migration
+ def self.up
+ add_index :custom_values, :custom_field_id
+ end
+
+ def self.down
+ remove_index :custom_values, :custom_field_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToJournals < ActiveRecord::Migration
+ def self.up
+ add_index :journals, :user_id
+ add_index :journals, :journalized_id
+ end
+
+ def self.down
+ remove_index :journals, :user_id
+ remove_index :journals, :journalized_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToIssueRelations < ActiveRecord::Migration
+ def self.up
+ add_index :issue_relations, :issue_from_id
+ add_index :issue_relations, :issue_to_id
+ end
+
+ def self.down
+ remove_index :issue_relations, :issue_from_id
+ remove_index :issue_relations, :issue_to_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToWikiRedirects < ActiveRecord::Migration
+ def self.up
+ add_index :wiki_redirects, :wiki_id
+ end
+
+ def self.down
+ remove_index :wiki_redirects, :wiki_id
+ end
+end
--- /dev/null
+class AddMissingIndexesToCustomFieldsTrackers < ActiveRecord::Migration
+ def self.up
+ add_index :custom_fields_trackers, [:custom_field_id, :tracker_id]
+ end
+
+ def self.down
+ remove_index :custom_fields_trackers, :column => [:custom_field_id, :tracker_id]
+ end
+end
--- /dev/null
+class AddActivityIndexes < ActiveRecord::Migration\r
+ def self.up\r
+ add_index :journals, :created_on\r
+ add_index :changesets, :committed_on\r
+ add_index :wiki_content_versions, :updated_on\r
+ add_index :messages, :created_on\r
+ add_index :issues, :created_on\r
+ add_index :news, :created_on\r
+ add_index :attachments, :created_on\r
+ add_index :documents, :created_on\r
+ add_index :time_entries, :created_on\r
+ end\r
+\r
+ def self.down\r
+ remove_index :journals, :created_on\r
+ remove_index :changesets, :committed_on\r
+ remove_index :wiki_content_versions, :updated_on\r
+ remove_index :messages, :created_on\r
+ remove_index :issues, :created_on\r
+ remove_index :news, :created_on\r
+ remove_index :attachments, :created_on\r
+ remove_index :documents, :created_on\r
+ remove_index :time_entries, :created_on\r
+ end\r
+end\r
--- /dev/null
+class AddVersionsStatus < ActiveRecord::Migration\r
+ def self.up\r
+ add_column :versions, :status, :string, :default => 'open'\r
+ end\r
+\r
+ def self.down\r
+ remove_column :versions, :status\r
+ end\r
+end\r
--- /dev/null
+class AddViewIssuesPermission < ActiveRecord::Migration\r
+ def self.up\r
+ Role.find(:all).each do |r|\r
+ r.add_permission!(:view_issues)\r
+ end\r
+ end\r
+\r
+ def self.down\r
+ Role.find(:all).each do |r|\r
+ r.remove_permission!(:view_issues)\r
+ end\r
+ end\r
+end\r
--- /dev/null
+== Redmine changelog
+
+Redmine - project management software
+Copyright (C) 2006-2009 Jean-Philippe Lang
+http://www.redmine.org/
+
+
+== 2009-02-15 v0.8.1
+
+* Select watchers on new issue form
+* Issue description is no longer a required field
+* Files module: ability to add files without version
+* Jump to the current tab when using the project quick-jump combo
+* Display a warning if some attachments were not saved
+* Import custom fields values from emails on issue creation
+* Show view/annotate/download links on entry and annotate views
+* Admin Info Screen: Display if plugin assets directory is writable
+* Adds a 'Create and continue' button on the new issue form
+* IMAP: add options to move received emails
+* Do not show Category field when categories are not defined
+* Lower the project identifier limit to a minimum of two characters
+* Add "closed" html class to closed entries in issue list
+* Fixed: broken redirect URL on login failure
+* Fixed: Deleted files are shown when using Darcs
+* Fixed: Darcs adapter works on Win32 only
+* Fixed: syntax highlight doesn't appear in new ticket preview
+* Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
+* Fixed: no error is raised when entering invalid hours on the issue update form
+* Fixed: Details time log report CSV export doesn't honour date format from settings
+* Fixed: invalid css classes on issue details
+* Fixed: Trac importer creates duplicate custom values
+* Fixed: inline attached image should not match partial filename
+
+
+== 2008-12-30 v0.8.0
+
+* Setting added in order to limit the number of diff lines that should be displayed
+* Makes logged-in username in topbar linking to
+* Mail handler: strip tags when receiving a html-only email
+* Mail handler: add watchers before sending notification
+* Adds a css class (overdue) to overdue issues on issue lists and detail views
+* Fixed: project activity truncated after viewing user's activity
+* Fixed: email address entered for password recovery shouldn't be case-sensitive
+* Fixed: default flag removed when editing a default enumeration
+* Fixed: default category ignored when adding a document
+* Fixed: error on repository user mapping when a repository username is blank
+* Fixed: Firefox cuts off large diffs
+* Fixed: CVS browser should not show dead revisions (deleted files)
+* Fixed: escape double-quotes in image titles
+* Fixed: escape textarea content when editing a issue note
+* Fixed: JS error on context menu with IE
+* Fixed: bold syntax around single character in series doesn't work
+* Fixed several XSS vulnerabilities
+* Fixed a SQL injection vulnerability
+
+
+== 2008-12-07 v0.8.0-rc1
+
+* Wiki page protection
+* Wiki page hierarchy. Parent page can be assigned on the Rename screen
+* Adds support for issue creation via email
+* Adds support for free ticket filtering and custom queries on Gantt chart and calendar
+* Cross-project search
+* Ability to search a project and its subprojects
+* Ability to search the projects the user belongs to
+* Adds custom fields on time entries
+* Adds boolean and list custom fields for time entries as criteria on time report
+* Cross-project time reports
+* Display latest user's activity on account/show view
+* Show last connexion time on user's page
+* Obfuscates email address on user's account page using javascript
+* wiki TOC rendered as an unordered list
+* Adds the ability to search for a user on the administration users list
+* Adds the ability to search for a project name or identifier on the administration projects list
+* Redirect user to the previous page after logging in
+* Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
+* Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
+* Adds permissions to let users edit and/or delete their messages
+* Link to activity view when displaying dates
+* Hide Redmine version in atom feeds and pdf properties
+* Maps repository users to Redmine users. Users with same username or email are automatically mapped. Mapping can be manually adjusted in repository settings. Multiple usernames can be mapped to the same Redmine user.
+* Sort users by their display names so that user dropdown lists are sorted alphabetically
+* Adds estimated hours to issue filters
+* Switch order of current and previous revisions in side-by-side diff
+* Render the commit changes list as a tree
+* Adds watch/unwatch functionality at forum topic level
+* When moving an issue to another project, reassign it to the category with same name if any
+* Adds child_pages macro for wiki pages
+* Use GET instead of POST on roadmap (#718), gantt and calendar forms
+* Search engine: display total results count and count by result type
+* Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
+* Adds icons on search results
+* Adds 'Edit' link on account/show for admin users
+* Adds Lock/Unlock/Activate link on user edit screen
+* Adds user count in status drop down on admin user list
+* Adds multi-levels blockquotes support by using > at the beginning of lines
+* Adds a Reply link to each issue note
+* Adds plain text only option for mail notifications
+* Gravatar support for issue detail, user grid, and activity stream (disabled by default)
+* Adds 'Delete wiki pages attachments' permission
+* Show the most recent file when displaying an inline image
+* Makes permission screens localized
+* AuthSource list: display associated users count and disable 'Delete' buton if any
+* Make the 'duplicates of' relation asymmetric
+* Adds username to the password reminder email
+* Adds links to forum messages using message#id syntax
+* Allow same name for custom fields on different object types
+* One-click bulk edition using the issue list context menu within the same project
+* Adds support for commit logs reencoding to UTF-8 before insertion in the database. Source encoding of commit logs can be selected in Application settings -> Repositories.
+* Adds checkboxes toggle links on permissions report
+* Adds Trac-Like anchors on wiki headings
+* Adds support for wiki links with anchor
+* Adds category to the issue context menu
+* Adds a workflow overview screen
+* Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
+* Dots allowed in custom field name
+* Adds posts quoting functionality
+* Adds an option to generate sequential project identifiers
+* Adds mailto link on the user administration list
+* Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
+* Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
+* Change projects homepage limit to 255 chars
+* Improved on-the-fly account creation. If some attributes are missing (eg. not present in the LDAP) or are invalid, the registration form is displayed so that the user is able to fill or fix these attributes
+* Adds "please select" to activity select box if no activity is set as default
+* Do not silently ignore timelog validation failure on issue edit
+* Adds a rake task to send reminder emails
+* Allow empty cells in wiki tables
+* Makes wiki text formatter pluggable
+* Adds back textile acronyms support
+* Remove pre tag attributes
+* Plugin hooks
+* Pluggable admin menu
+* Plugins can provide activity content
+* Moves plugin list to its own administration menu item
+* Adds url and author_url plugin attributes
+* Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
+* Adds atom feed on time entries details
+* Adds project name to issues feed title
+* Adds a css class on menu items in order to apply item specific styles (eg. icons)
+* Adds a Redmine plugin generators
+* Adds timelog link to the issue context menu
+* Adds links to the user page on various views
+* Turkish translation by Ismail Sezen
+* Catalan translation
+* Vietnamese translation
+* Slovak translation
+* Better naming of activity feed if only one kind of event is displayed
+* Enable syntax highlight on issues, messages and news
+* Add target version to the issue list context menu
+* Hide 'Target version' filter if no version is defined
+* Add filters on cross-project issue list for custom fields marked as 'For all projects'
+* Turn ftp urls into links
+* Hiding the View Differences button when a wiki page's history only has one version
+* Messages on a Board can now be sorted by the number of replies
+* Adds a class ('me') to events of the activity view created by current user
+* Strip pre/code tags content from activity view events
+* Display issue notes in the activity view
+* Adds links to changesets atom feed on repository browser
+* Track project and tracker changes in issue history
+* Adds anchor to atom feed messages links
+* Adds a key in lang files to set the decimal separator (point or comma) in csv exports
+* Makes importer work with Trac 0.8.x
+* Upgraded to Prototype 1.6.0.1
+* File viewer for attached text files
+* Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
+* Removed inconsistent revision numbers on diff view
+* CVS: add support for modules names with spaces
+* Log the user in after registration if account activation is not needed
+* Mercurial adapter improvements
+* Trac importer: read session_attribute table to find user's email and real name
+* Ability to disable unused SCM adapters in application settings
+* Adds Filesystem adapter
+* Clear changesets and changes with raw sql when deleting a repository for performance
+* Redmine.pm now uses the 'commit access' permission defined in Redmine
+* Reposman can create any type of scm (--scm option)
+* Reposman creates a repository if the 'repository' module is enabled at project level only
+* Display svn properties in the browser, svn >= 1.5.0 only
+* Reduces memory usage when importing large git repositories
+* Wider SVG graphs in repository stats
+* SubversionAdapter#entries performance improvement
+* SCM browser: ability to download raw unified diffs
+* More detailed error message in log when scm command fails
+* Adds support for file viewing with Darcs 2.0+
+* Check that git changeset is not in the database before creating it
+* Unified diff viewer for attached files with .patch or .diff extension
+* File size display with Bazaar repositories
+* Git adapter: use commit time instead of author time
+* Prettier url for changesets
+* Makes changes link to entries on the revision view
+* Adds a field on the repository view to browse at specific revision
+* Adds new projects atom feed
+* Added rake tasks to generate rcov code coverage reports
+* Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
+* Show the project hierarchy in the drop down list for new membership on user administration screen
+* Split user edit screen into tabs
+* Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
+* Fixed: Roadmap crashes when a version has a due date > 2037
+* Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
+* Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
+* Fixed: logtime entry duplicated when edited from parent project
+* Fixed: wrong digest for text files under Windows
+* Fixed: associated revisions are displayed in wrong order on issue view
+* Fixed: Git Adapter date parsing ignores timezone
+* Fixed: Printing long roadmap doesn't split across pages
+* Fixes custom fields display order at several places
+* Fixed: urls containing @ are parsed as email adress by the wiki formatter
+* Fixed date filters accuracy with SQLite
+* Fixed: tokens not escaped in highlight_tokens regexp
+* Fixed Bazaar shared repository browsing
+* Fixes platform determination under JRuby
+* Fixed: Estimated time in issue's journal should be rounded to two decimals
+* Fixed: 'search titles only' box ignored after one search is done on titles only
+* Fixed: non-ASCII subversion path can't be displayed
+* Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
+* Fixed: document listing shows on "my page" when viewing documents is disabled for the role
+* Fixed: Latest news appear on the homepage for projects with the News module disabled
+* Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
+* Fixed: the default status is lost when reordering issue statuses
+* Fixes error with Postgresql and non-UTF8 commit logs
+* Fixed: textile footnotes no longer work
+* Fixed: http links containing parentheses fail to reder correctly
+* Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
+
+
+== 2008-07-06 v0.7.3
+
+* Allow dot in firstnames and lastnames
+* Add project name to cross-project Atom feeds
+* Encoding set to utf8 in example database.yml
+* HTML titles on forums related views
+* Fixed: various XSS vulnerabilities
+* Fixed: Entourage (and some old client) fails to correctly render notification styles
+* Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
+* Fixed: wrong relative paths to images in wiki_syntax.html
+
+
+== 2008-06-15 v0.7.2
+
+* "New Project" link on Projects page
+* Links to repository directories on the repo browser
+* Move status to front in Activity View
+* Remove edit step from Status context menu
+* Fixed: No way to do textile horizontal rule
+* Fixed: Repository: View differences doesn't work
+* Fixed: attachement's name maybe invalid.
+* Fixed: Error when creating a new issue
+* Fixed: NoMethodError on @available_filters.has_key?
+* Fixed: Check All / Uncheck All in Email Settings
+* Fixed: "View differences" of one file at /repositories/revision/ fails
+* Fixed: Column width in "my page"
+* Fixed: private subprojects are listed on Issues view
+* Fixed: Textile: bold, italics, underline, etc... not working after parentheses
+* Fixed: Update issue form: comment field from log time end out of screen
+* Fixed: Editing role: "issue can be assigned to this role" out of box
+* Fixed: Unable use angular braces after include word
+* Fixed: Using '*' as keyword for repository referencing keywords doesn't work
+* Fixed: Subversion repository "View differences" on each file rise ERROR
+* Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
+* Fixed: It is possible to lock out the last admin account
+* Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
+* Fixed: Issue number display clipped on 'my issues'
+* Fixed: Roadmap version list links not carrying state
+* Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
+* Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
+* Fixed: browser's language subcodes ignored
+* Fixed: Error on project selection with numeric (only) identifier.
+* Fixed: Link to PDF doesn't work after creating new issue
+* Fixed: "Replies" should not be shown on forum threads that are locked
+* Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
+* Fixed: http links containing hashes don't display correct
+* Fixed: Allow ampersands in Enumeration names
+* Fixed: Atom link on saved query does not include query_id
+* Fixed: Logtime info lost when there's an error updating an issue
+* Fixed: TOC does not parse colorization markups
+* Fixed: CVS: add support for modules names with spaces
+* Fixed: Bad rendering on projects/add
+* Fixed: exception when viewing differences on cvs
+* Fixed: export issue to pdf will messup when use Chinese language
+* Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
+* Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
+* Fixed: Importing from trac : some wiki links are messed
+* Fixed: Incorrect weekend definition in Hebrew calendar locale
+* Fixed: Atom feeds don't provide author section for repository revisions
+* Fixed: In Activity views, changesets titles can be multiline while they should not
+* Fixed: Ignore unreadable subversion directories (read disabled using authz)
+* Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
+* Fixed: Close statement handler in Redmine.pm
+
+
+== 2008-05-04 v0.7.1
+
+* Thai translation added (Gampol Thitinilnithi)
+* Translations updates
+* Escape HTML comment tags
+* Prevent "can't convert nil into String" error when :sort_order param is not present
+* Fixed: Updating tickets add a time log with zero hours
+* Fixed: private subprojects names are revealed on the project overview
+* Fixed: Search for target version of "none" fails with postgres 8.3
+* Fixed: Home, Logout, Login links shouldn't be absolute links
+* Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
+* Fixed: error when using upcase language name in coderay
+* Fixed: error on Trac import when :due attribute is nil
+
+
+== 2008-04-28 v0.7.0
+
+* Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
+* Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
+* Add predefined date ranges to the time report
+* Time report can be done at issue level
+* Various timelog report enhancements
+* Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
+* Display the context menu above and/or to the left of the click if needed
+* Make the admin project files list sortable
+* Mercurial: display working directory files sizes unless browsing a specific revision
+* Preserve status filter and page number when using lock/unlock/activate links on the users list
+* Redmine.pm support for LDAP authentication
+* Better error message and AR errors in log for failed LDAP on-the-fly user creation
+* Redirected user to where he is coming from after logging hours
+* Warn user that subprojects are also deleted when deleting a project
+* Include subprojects versions on calendar and gantt
+* Notify project members when a message is posted if they want to receive notifications
+* Fixed: Feed content limit setting has no effect
+* Fixed: Priorities not ordered when displayed as a filter in issue list
+* Fixed: can not display attached images inline in message replies
+* Fixed: Boards are not deleted when project is deleted
+* Fixed: trying to preview a new issue raises an exception with postgresql
+* Fixed: single file 'View difference' links do not work because of duplicate slashes in url
+* Fixed: inline image not displayed when including a wiki page
+* Fixed: CVS duplicate key violation
+* Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
+* Fixed: custom field filters behaviour
+* Fixed: Postgresql 8.3 compatibility
+* Fixed: Links to repository directories don't work
+
+
+== 2008-03-29 v0.7.0-rc1
+
+* Overall activity view and feed added, link is available on the project list
+* Git VCS support
+* Rails 2.0 sessions cookie store compatibility
+* Use project identifiers in urls instead of ids
+* Default configuration data can now be loaded from the administration screen
+* Administration settings screen split to tabs (email notifications options moved to 'Settings')
+* Project description is now unlimited and optional
+* Wiki annotate view
+* Escape HTML tag in textile content
+* Add Redmine links to documents, versions, attachments and repository files
+* New setting to specify how many objects should be displayed on paginated lists. There are 2 ways to select a set of issues on the issue list:
+ * by using checkbox and/or the little pencil that will select/unselect all issues
+ * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
+* Context menu disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (click anywhere else on the row to display the context menu)
+* User display format is now configurable in administration settings
+* Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
+* Merged 'change status', 'edit issue' and 'add note' actions:
+ * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
+ * 'Change issue status' permission removed. To change an issue status, a user just needs to have either 'Edit' or 'Add note' permissions and some workflow transitions allowed
+* Details by assignees on issue summary view
+* 'New issue' link in the main menu (accesskey 7). The drop-down lists to add an issue on the project overview and the issue list are removed
+* Change status select box default to current status
+* Preview for issue notes, news and messages
+* Optional description for attachments
+* 'Fixed version' label changed to 'Target version'
+* Let the user choose when deleting issues with reported hours to:
+ * delete the hours
+ * assign the hours to the project
+ * reassign the hours to another issue
+* Date range filter and pagination on time entries detail view
+* Propagate time tracking to the parent project
+* Switch added on the project activity view to include subprojects
+* Display total estimated and spent hours on the version detail view
+* Weekly time tracking block for 'My page'
+* Permissions to edit time entries
+* Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
+* Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
+* Make versions with same date sorted by name
+* Allow issue list to be sorted by target version
+* Related changesets messages displayed on the issue details view
+* Create a journal and send an email when an issue is closed by commit
+* Add 'Author' to the available columns for the issue list
+* More appropriate default sort order on sortable columns
+* Add issue subject to the time entries view and issue subject, description and tracker to the csv export
+* Permissions to edit issue notes
+* Display date/time instead of date on files list
+* Do not show Roadmap menu item if the project doesn't define any versions
+* Allow longer version names (60 chars)
+* Ability to copy an existing workflow when creating a new role
+* Display custom fields in two columns on the issue form
+* Added 'estimated time' in the csv export of the issue list
+* Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings)
+* Setting for whether new projects should be public by default
+* User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
+* Added default value for custom fields
+* Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
+* Redirect to issue page after creating a new issue
+* Wiki toolbar improvements (mainly for Firefox)
+* Display wiki syntax quick ref link on all wiki textareas
+* Display links to Atom feeds
+* Breadcrumb nav for the forums
+* Show replies when choosing to display messages in the activity
+* Added 'include' macro to include another wiki page
+* RedmineWikiFormatting page available as a static HTML file locally
+* Wrap diff content
+* Strip out email address from authors in repository screens
+* Highlight the current item of the main menu
+* Added simple syntax highlighters for php and java languages
+* Do not show empty diffs
+* Show explicit error message when the scm command failed (eg. when svn binary is not available)
+* Lithuanian translation added (Sergej Jegorov)
+* Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
+* Danish translation added (Mads Vestergaard)
+* Added i18n support to the jstoolbar and various settings screen
+* RedCloth's glyphs no longer user
+* New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
+* The following menus can now be extended by plugins: top_menu, account_menu, application_menu
+* Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
+* Remove hardcoded "Redmine" strings in account related emails and use application title instead
+* Mantis importer preserve bug ids
+* Trac importer: Trac guide wiki pages skipped
+* Trac importer: wiki attachments migration added
+* Trac importer: support database schema for Trac migration
+* Trac importer: support CamelCase links
+* Removes the Redmine version from the footer (can be viewed on admin -> info)
+* Rescue and display an error message when trying to delete a role that is in use
+* Add various 'X-Redmine' headers to email notifications: X-Redmine-Host, X-Redmine-Site, X-Redmine-Project, X-Redmine-Issue-Id, -Author, -Assignee, X-Redmine-Topic-Id
+* Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
+* Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
+* Fixed: Textile image with style attribute cause internal server error
+* Fixed: wiki TOC not rendered properly when used in an issue or document description
+* Fixed: 'has already been taken' error message on username and email fields if left empty
+* Fixed: non-ascii attachement filename with IE
+* Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
+* Fixed: search for all words doesn't work
+* Fixed: Do not show sticky and locked checkboxes when replying to a message
+* Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
+* Fixed: Date custom fields not displayed as specified in application settings
+* Fixed: titles not escaped in the activity view
+* Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
+* Fixed: on calendar, gantt and in the tracker filter on the issue list, only active trackers of the project (and its sub projects) should be available
+* Fixed: locked users should not receive email notifications
+* Fixed: custom field selection is not saved when unchecking them all on project settings
+* Fixed: can not lock a topic when creating it
+* Fixed: Incorrect filtering for unset values when using 'is not' filter
+* Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
+* Fixed: ajax pagination does not scroll up
+* Fixed: error when uploading a file with no content-type specified by the browser
+* Fixed: wiki and changeset links not displayed when previewing issue description or notes
+* Fixed: 'LdapError: no bind result' error when authenticating
+* Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
+* Fixed: CVS repository doesn't work if port is used in the url
+* Fixed: Email notifications: host name is missing in generated links
+* Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
+* Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
+* Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
+* Fixed: Do not send an email with no recipient, cc or bcc
+* Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
+* Fixed: Mercurial browsing under unix-like os and for directory depth > 2
+* Fixed: Wiki links with pipe can not be used in wiki tables
+* Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
+* Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
+
+
+== 2008-03-12 v0.6.4
+
+* Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
+* Fixed: potential LDAP authentication security flaw
+* Fixed: context submenus on the issue list don't show up with IE6.
+* Fixed: Themes are not applied with Rails 2.0
+* Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
+* Fixed: Mercurial repository browsing
+* Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
+* Fixed: not null constraints not removed with Postgresql
+* Doctype set to transitional
+
+
+== 2007-12-18 v0.6.3
+
+* Fixed: upload doesn't work in 'Files' section
+
+
+== 2007-12-16 v0.6.2
+
+* Search engine: issue custom fields can now be searched
+* News comments are now textilized
+* Updated Japanese translation (Satoru Kurashiki)
+* Updated Chinese translation (Shortie Lo)
+* Fixed Rails 2.0 compatibility bugs:
+ * Unable to create a wiki
+ * Gantt and calendar error
+ * Trac importer error (readonly? is defined by ActiveRecord)
+* Fixed: 'assigned to me' filter broken
+* Fixed: crash when validation fails on issue edition with no custom fields
+* Fixed: reposman "can't find group" error
+* Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
+* Fixed: empty lines when displaying repository files with Windows style eol
+* Fixed: missing body closing tag in repository annotate and entry views
+
+
+== 2007-12-10 v0.6.1
+
+* Rails 2.0 compatibility
+* Custom fields can now be displayed as columns on the issue list
+* Added version details view (accessible from the roadmap)
+* Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
+* Added per-project tracker selection. Trackers can be selected on project settings
+* Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
+* Forums: messages can now be edited/deleted (explicit permissions need to be given)
+* Forums: topics can be locked so that no reply can be added
+* Forums: topics can be marked as sticky so that they always appear at the top of the list
+* Forums: attachments can now be added to replies
+* Added time zone support
+* Added a setting to choose the account activation strategy (available in application settings)
+* Added 'Classic' theme (inspired from the v0.51 design)
+* Added an alternate theme which provides issue list colorization based on issues priority
+* Added Bazaar SCM adapter
+* Added Annotate/Blame view in the repository browser (except for Darcs SCM)
+* Diff style (inline or side by side) automatically saved as a user preference
+* Added issues status changes on the activity view (by Cyril Mougel)
+* Added forums topics on the activity view (disabled by default)
+* Added an option on 'My account' for users who don't want to be notified of changes that they make
+* Trac importer now supports mysql and postgresql databases
+* Trac importer improvements (by Mat Trudel)
+* 'fixed version' field can now be displayed on the issue list
+* Added a couple of new formats for the 'date format' setting
+* Added Traditional Chinese translation (by Shortie Lo)
+* Added Russian translation (iGor kMeta)
+* Project name format limitation removed (name can now contain any character)
+* Project identifier maximum length changed from 12 to 20
+* Changed the maximum length of LDAP account to 255 characters
+* Removed the 12 characters limit on passwords
+* Added wiki macros support
+* Performance improvement on workflow setup screen
+* More detailed html title on several views
+* Custom fields can now be reordered
+* Search engine: search can be restricted to an exact phrase by using quotation marks
+* Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
+* Email notifications are now sent as Blind carbon copy by default
+* Fixed: all members (including non active) should be deleted when deleting a project
+* Fixed: Error on wiki syntax link (accessible from wiki/edit)
+* Fixed: 'quick jump to a revision' form on the revisions list
+* Fixed: error on admin/info if there's more than 1 plugin installed
+* Fixed: svn or ldap password can be found in clear text in the html source in editing mode
+* Fixed: 'Assigned to' drop down list is not sorted
+* Fixed: 'View all issues' link doesn't work on issues/show
+* Fixed: error on account/register when validation fails
+* Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
+* Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
+* Fixed: Wrong feed URLs on the home page
+* Fixed: Update of time entry fails when the issue has been moved to an other project
+* Fixed: Error when moving an issue without changing its tracker (Postgresql)
+* Fixed: Changes not recorded when using :pserver string (CVS adapter)
+* Fixed: admin should be able to move issues to any project
+* Fixed: adding an attachment is not possible when changing the status of an issue
+* Fixed: No mime-types in documents/files downloading
+* Fixed: error when sorting the messages if there's only one board for the project
+* Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
+
+== 2007-11-04 v0.6.0
+
+* Permission model refactoring.
+* Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
+* Permissions: some permissions (eg. browse the repository) can be removed for certain roles
+* Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
+* Added Mantis and Trac importers
+* New application layout
+* Added "Bulk edit" functionality on the issue list
+* More flexible mail notifications settings at user level
+* Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue
+* Added the hability to copy an issue. It can be done from the "issue/show" view or from the context menu on the issue list
+* Added the ability to customize issue list columns (at application level or for each saved query)
+* Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
+* Added the ability to rename wiki pages (specific permission required)
+* Search engines now supports pagination. Results are sorted in reverse chronological order
+* Added "Estimated hours" attribute on issues
+* A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
+* Forum notifications are now also sent to the authors of the thread, even if they don\92t watch the board
+* Added an application setting to specify the application protocol (http or https) used to generate urls in emails
+* Gantt chart: now starts at the current month by default
+* Gantt chart: month count and zoom factor are automatically saved as user preferences
+* Wiki links can now refer to other project wikis
+* Added wiki index by date
+* Added preview on add/edit issue form
+* Emails footer can now be customized from the admin interface (Admin -> Email notifications)
+* Default encodings for repository files can now be set in application settings (used to convert files content and diff to UTF-8 so that they\92re properly displayed)
+* Calendar: first day of week can now be set in lang files
+* Automatic closing of duplicate issues
+* Added a cross-project issue list
+* AJAXified the SCM browser (tree view)
+* Pretty URL for the repository browser (Cyril Mougel)
+* Search engine: added a checkbox to search titles only
+* Added "% done" in the filter list
+* Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
+* Added some accesskeys
+* Added "Float" as a custom field format
+* Added basic Theme support
+* Added the ability to set the \93done ratio\94 of issues fixed by commit (Nikolay Solakov)
+* Added custom fields in issue related mail notifications
+* Email notifications are now sent in plain text and html
+* Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
+* Added syntax highlightment for repository files and wiki
+* Improved automatic Redmine links
+* Added automatic table of content support on wiki pages
+* Added radio buttons on the documents list to sort documents by category, date, title or author
+* Added basic plugin support, with a sample plugin
+* Added a link to add a new category when creating or editing an issue
+* Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
+* Added an option to be able to relate issues in different projects
+* Added the ability to move issues (to another project) without changing their trackers.
+* Atom feeds added on project activity, news and changesets
+* Added the ability to reset its own RSS access key
+* Main project list now displays root projects with their subprojects
+* Added anchor links to issue notes
+* Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
+* Issue notes are now included in search
+* Added email sending test functionality
+* Added LDAPS support for LDAP authentication
+* Removed hard-coded URLs in mail templates
+* Subprojects are now grouped by projects in the navigation drop-down menu
+* Added a new value for date filters: this week
+* Added cache for application settings
+* Added Polish translation (Tomasz Gawryl)
+* Added Czech translation (Jan Kadlecek)
+* Added Romanian translation (Csongor Bartus)
+* Added Hebrew translation (Bob Builder)
+* Added Serbian translation (Dragan Matic)
+* Added Korean translation (Choi Jong Yoon)
+* Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
+* Performance improvement on calendar and gantt
+* Fixed: wiki preview doesn\92t work on long entries
+* Fixed: queries with multiple custom fields return no result
+* Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
+* Fixed: URL with ~ broken in wiki formatting
+* Fixed: some quotation marks are rendered as strange characters in pdf
+
+
+== 2007-07-15 v0.5.1
+
+* per project forums added
+* added the ability to archive projects
+* added \93Watch\94 functionality on issues. It allows users to receive notifications about issue changes
+* custom fields for issues can now be used as filters on issue list
+* added per user custom queries
+* commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
+* projects list now shows the list of public projects and private projects for which the user is a member
+* versions can now be created with no date
+* added issue count details for versions on Reports view
+* added time report, by member/activity/tracker/version and year/month/week for the selected period
+* each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
+* added autologin feature (disabled by default)
+* optimistic locking added for wiki edits
+* added wiki diff
+* added the ability to destroy wiki pages (requires permission)
+* a wiki page can now be attached to each version, and displayed on the roadmap
+* attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
+* added an option to see all versions in the roadmap view (including completed ones)
+* added basic issue relations
+* added the ability to log time when changing an issue status
+* account information can now be sent to the user when creating an account
+* author and assignee of an issue always receive notifications (even if they turned of mail notifications)
+* added a quick search form in page header
+* added 'me' value for 'assigned to' and 'author' query filters
+* added a link on revision screen to see the entire diff for the revision
+* added last commit message for each entry in repository browser
+* added the ability to view a file diff with free to/from revision selection.
+* text files can now be viewed online when browsing the repository
+* added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
+* added fragment caching for svn diffs
+* added fragment caching for calendar and gantt views
+* login field automatically focused on login form
+* subproject name displayed on issue list, calendar and gantt
+* added an option to choose the date format: language based or ISO 8601
+* added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
+* a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
+* added portuguese translation (Joao Carlos Clementoni)
+* added partial online help japanese translation (Ken Date)
+* added bulgarian translation (Nikolay Solakov)
+* added dutch translation (Linda van den Brink)
+* added swedish translation (Thomas Habets)
+* italian translation update (Alessio Spadaro)
+* japanese translation update (Satoru Kurashiki)
+* fixed: error on history atom feed when there\92s no notes on an issue change
+* fixed: error in journalizing an issue with longtext custom fields (Postgresql)
+* fixed: creation of Oracle schema
+* fixed: last day of the month not included in project activity
+* fixed: files with an apostrophe in their names can't be accessed in SVN repository
+* fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
+* fixed: open/closed issue counts are always 0 on reports view (postgresql)
+* fixed: date query filters (wrong results and sql error with postgresql)
+* fixed: confidentiality issue on account/show (private project names displayed to anyone)
+* fixed: Long text custom fields displayed without line breaks
+* fixed: Error when editing the wokflow after deleting a status
+* fixed: SVN commit dates are now stored as local time
+
+
+== 2007-04-11 v0.5.0
+
+* added per project Wiki
+* added rss/atom feeds at project level (custom queries can be used as feeds)
+* added search engine (search in issues, news, commits, wiki pages, documents)
+* simple time tracking functionality added
+* added version due dates on calendar and gantt
+* added subprojects issue count on project Reports page
+* added the ability to copy an existing workflow when creating a new tracker
+* added the ability to include subprojects on calendar and gantt
+* added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
+* added side by side svn diff view (Cyril Mougel)
+* added back subproject filter on issue list
+* added permissions report in admin area
+* added a status filter on users list
+* support for password-protected SVN repositories
+* SVN commits are now stored in the database
+* added simple svn statistics SVG graphs
+* progress bars for roadmap versions (Nick Read)
+* issue history now shows file uploads and deletions
+* #id patterns are turned into links to issues in descriptions and commit messages
+* japanese translation added (Satoru Kurashiki)
+* chinese simplified translation added (Andy Wu)
+* italian translation added (Alessio Spadaro)
+* added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
+* better calendar rendering time
+* fixed migration scripts to work with mysql 5 running in strict mode
+* fixed: error when clicking "add" with no block selected on my/page_layout
+* fixed: hard coded links in navigation bar
+* fixed: table_name pre/suffix support
+
+
+== 2007-02-18 v0.4.2
+
+* Rails 1.2 is now required
+* settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
+* added project roadmap view
+* mail notifications added when a document, a file or an attachment is added
+* tooltips added on Gantt chart and calender to view the details of the issues
+* ability to set the sort order for roles, trackers, issue statuses
+* added missing fields to csv export: priority, start date, due date, done ratio
+* added total number of issues per tracker on project overview
+* all icons replaced (new icons are based on GPL icon set: "KDE Crystal Diamond 2.5" -by paolino- and "kNeu! Alpha v0.1" -by Pablo Fabregat-)
+* added back "fixed version" field on issue screen and in filters
+* project settings screen split in 4 tabs
+* custom fields screen split in 3 tabs (one for each kind of custom field)
+* multiple issues pdf export now rendered as a table
+* added a button on users/list to manually activate an account
+* added a setting option to disable "password lost" functionality
+* added a setting option to set max number of issues in csv/pdf exports
+* fixed: subprojects count is always 0 on projects list
+* fixed: locked users are proposed when adding a member to a project
+* fixed: setting an issue status as default status leads to an sql error with SQLite
+* fixed: unable to delete an issue status even if it's not used yet
+* fixed: filters ignored when exporting a predefined query to csv/pdf
+* fixed: crash when french "issue_edit" email notification is sent
+* fixed: hide mail preference not saved (my/account)
+* fixed: crash when a new user try to edit its "my page" layout
+
+
+== 2007-01-03 v0.4.1
+
+* fixed: emails have no recipient when one of the project members has notifications disabled
+
+
+== 2007-01-02 v0.4.0
+
+* simple SVN browser added (just needs svn binaries in PATH)
+* comments can now be added on news
+* "my page" is now customizable
+* more powerfull and savable filters for issues lists
+* improved issues change history
+* new functionality: move an issue to another project or tracker
+* new functionality: add a note to an issue
+* new report: project activity
+* "start date" and "% done" fields added on issues
+* project calendar added
+* gantt chart added (exportable to pdf)
+* single/multiple issues pdf export added
+* issues reports improvements
+* multiple file upload for issues, documents and files
+* option to set maximum size of uploaded files
+* textile formating of issue and news descritions (RedCloth required)
+* integration of DotClear jstoolbar for textile formatting
+* calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
+* new filter in issues list: Author
+* ajaxified paginators
+* news rss feed added
+* option to set number of results per page on issues list
+* localized csv separator (comma/semicolon)
+* csv output encoded to ISO-8859-1
+* user custom field displayed on account/show
+* default configuration improved (default roles, trackers, status, permissions and workflows)
+* language for default configuration data can now be chosen when running 'load_default_data' task
+* javascript added on custom field form to show/hide fields according to the format of custom field
+* fixed: custom fields not in csv exports
+* fixed: project settings now displayed according to user's permissions
+* fixed: application error when no version is selected on projects/add_file
+* fixed: public actions not authorized for members of non public projects
+* fixed: non public projects were shown on welcome screen even if current user is not a member
+
+
+== 2006-10-08 v0.3.0
+
+* user authentication against multiple LDAP (optional)
+* token based "lost password" functionality
+* user self-registration functionality (optional)
+* custom fields now available for issues, users and projects
+* new custom field format "text" (displayed as a textarea field)
+* project & administration drop down menus in navigation bar for quicker access
+* text formatting is preserved for long text fields (issues, projects and news descriptions)
+* urls and emails are turned into clickable links in long text fields
+* "due date" field added on issues
+* tracker selection filter added on change log
+* Localization plugin replaced with GLoc 1.1.0 (iconv required)
+* error messages internationalization
+* german translation added (thanks to Karim Trott)
+* data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
+* new filter in issues list: "Fixed version"
+* active filters are displayed with colored background on issues list
+* custom configuration is now defined in config/config_custom.rb
+* user object no more stored in session (only user_id)
+* news summary field is no longer required
+* tables and forms redesign
+* Fixed: boolean custom field not working
+* Fixed: error messages for custom fields are not displayed
+* Fixed: invalid custom fields should have a red border
+* Fixed: custom fields values are not validated on issue update
+* Fixed: unable to choose an empty value for 'List' custom fields
+* Fixed: no issue categories sorting
+* Fixed: incorrect versions sorting
+
+
+== 2006-07-12 - v0.2.2
+
+* Fixed: bug in "issues list"
+
+
+== 2006-07-09 - v0.2.1
+
+* new databases supported: Oracle, PostgreSQL, SQL Server
+* projects/subprojects hierarchy (1 level of subprojects only)
+* environment information display in admin/info
+* more filter options in issues list (rev6)
+* default language based on browser settings (Accept-Language HTTP header)
+* issues list exportable to CSV (rev6)
+* simple_format and auto_link on long text fields
+* more data validations
+* Fixed: error when all mail notifications are unchecked in admin/mail_options
+* Fixed: all project news are displayed on project summary
+* Fixed: Can't change user password in users/edit
+* Fixed: Error on tables creation with PostgreSQL (rev5)
+* Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
+
+
+== 2006-06-25 - v0.1.0
+
+* multiple users/multiple projects
+* role based access control
+* issue tracking system
+* fully customizable workflow
+* documents/files repository
+* email notifications on issue creation and update
+* multilanguage support (except for error messages):english, french, spanish
+* online manual in french (unfinished)
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
--- /dev/null
+== Redmine installation
+
+Redmine - project management software
+Copyright (C) 2006-2008 Jean-Philippe Lang
+http://www.redmine.org/
+
+
+== Requirements
+
+* Ruby on Rails 2.2.2
+* A database:
+ * MySQL (tested with MySQL 5)
+ * PostgreSQL (tested with PostgreSQL 8.1)
+ * SQLite (tested with SQLite 3)
+
+Optional:
+* SVN binaries >= 1.3 (needed for repository browsing, must be available in PATH)
+* RMagick (gantt export to png)
+
+== Installation
+
+1. Uncompress the program archive
+
+2. Create an empty database: "redmine" for example
+
+3. Configure database parameters in config/database.yml
+ for "production" environment (default database is MySQL)
+
+4. Create the database structure. Under the application main directory:
+ rake db:migrate RAILS_ENV="production"
+ It will create tables and an administrator account.
+
+5. Generate a session store secret
+ Redmine stores session data in cookies by default, which requires
+ a secret to be generated. Run:
+ rake config/initializers/session_store.rb
+
+6. Setting up permissions
+ The user who runs Redmine must have write permission on the following
+ subdirectories: files, log, tmp (create the last one if not present).
+
+ Assuming you run Redmine with a user named redmine:
+ mkdir tmp
+ sudo chown -R redmine:redmine files log tmp
+ sudo chmod -R 755 files log tmp
+
+7. Test the installation by running WEBrick web server:
+ ruby script/server -e production
+
+ Once WEBrick has started, point your browser to http://localhost:3000/
+ You should now see the application welcome page
+
+8. Use default administrator account to log in:
+ login: admin
+ password: admin
+
+ Go to "Administration" to load the default configuration data (roles,
+ trackers, statuses, workflow) and adjust application settings
+
+
+== Email delivery Configuration
+
+Copy config/email.yml.example to config/email.yml and edit this file
+to adjust your SMTP settings.
+Don't forget to restart the application after any change to this file.
+
+Please do not enter your SMTP settings in environment.rb.
--- /dev/null
+= Redmine
+
+Redmine is a flexible project management web application written using Ruby on Rails framework.
+
+More details can be found at http://www.redmine.org
--- /dev/null
+Installing gems for testing
+===========================
+
+Run `rake gems RAILS_ENV=test` to list the required gems. Run
+`rake gems:install RAILS_ENV=test` to install any missing gems.
+
+Running Tests
+=============
+
+Run `rake --tasks test` to see available tests.
+`rake test` will run the entire testsuite.
+
+Before running `rake test` you need to configure both development
+and test databases.
+
+Creating test repositories
+===================
+
+Redmine supports a wide array of different version control systems.
+To test the support, a test repository needs to be created for each of those.
+
+Run `rake --tasks test:scm:setup` for a list of available test-repositories or
+run `rake test:scm:setup:all` to set up all of them
+
--- /dev/null
+== Redmine upgrade procedure
+
+Redmine - project management software
+Copyright (C) 2006-2008 Jean-Philippe Lang
+http://www.redmine.org/
+
+
+== Upgrading
+
+1. Uncompress the program archive in a new directory
+
+3. Copy your database settings (RAILS_ROOT/config/database.yml)
+ and SMTP settings (RAILS_ROOT/config/email.yml)
+ into the new config directory
+
+4. Migrate your database (please make a backup before doing this):
+ rake db:migrate RAILS_ENV="production"
+
+5. Copy the RAILS_ROOT/files directory content into your new installation
+ This directory contains all the attached files
+
+
+== Notes
+
+1. Rails 2.1.2 is required for version 0.8.
+
+2. When upgrading your code with svn update, don't forget to clear
+ the application cache (RAILS_ROOT/tmp/cache) before restarting.
--- /dev/null
+#!/usr/bin/env ruby
+
+# == Synopsis
+#
+# Reads an email from standard input and forward it to a Redmine server
+# through a HTTP request.
+#
+# == Usage
+#
+# rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
+#
+# == Arguments
+#
+# -u, --url URL of the Redmine server
+# -k, --key Redmine API key
+#
+# 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
+# -h, --help show this help
+# -v, --verbose show extra information
+# -V, --version show version information and exit
+#
+# Issue attributes control options:
+# -p, --project=PROJECT identifier of the target project
+# -s, --status=STATUS name of the target status
+# -t, --tracker=TRACKER name of the target tracker
+# --category=CATEGORY name of the target category
+# --priority=PRIORITY name of the target priority
+# -o, --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:
+#
+# rdm-mailhandler --url http://redmine.domain.foo --key secret
+#
+# Fixed project and default tracker specified, but emails can override
+# both tracker and priority attributes using keywords:
+#
+# rdm-mailhandler --url https://domain.foo/redmine --key secret \\
+# --project foo \\
+# --tracker bug \\
+# --allow-override tracker,priority
+
+require 'net/http'
+require 'net/https'
+require 'uri'
+require 'getoptlong'
+require 'rdoc/usage'
+
+module Net
+ class HTTPS < HTTP
+ def self.post_form(url, params)
+ request = Post.new(url.path)
+ request.form_data = params
+ request.basic_auth url.user, url.password if url.user
+ http = new(url.host, url.port)
+ http.use_ssl = (url.scheme == 'https')
+ http.start {|h| h.request(request) }
+ end
+ end
+end
+
+class RedmineMailHandler
+ VERSION = '0.1'
+
+ attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :url, :key
+
+ def initialize
+ self.issue_attributes = {}
+
+ opts = GetoptLong.new(
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
+ [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
+ [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--status', '-s', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--category', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--priority', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--unknown-user', GetoptLong::REQUIRED_ARGUMENT]
+ )
+
+ opts.each do |opt, arg|
+ case opt
+ when '--url'
+ self.url = arg.dup
+ when '--key'
+ self.key = arg.dup
+ when '--help'
+ usage
+ when '--verbose'
+ self.verbose = true
+ when '--version'
+ puts VERSION; exit
+ when '--project', '--status', '--tracker', '--category', '--priority'
+ self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
+ when '--allow-override'
+ self.allow_override = arg.dup
+ when '--unknown-user'
+ self.unknown_user = arg.dup
+ end
+ end
+
+ RDoc.usage if url.nil?
+ end
+
+ def submit(email)
+ uri = url.gsub(%r{/*$}, '') + '/mail_handler'
+
+ data = { 'key' => key, 'email' => email,
+ 'allow_override' => allow_override,
+ 'unknown_user' => unknown_user }
+ issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
+
+ debug "Posting to #{uri}..."
+ response = Net::HTTPS.post_form(URI.parse(uri), data)
+ debug "Response received: #{response.code}"
+
+ puts "Request was denied by your Redmine server. " +
+ "Please, make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key." if response.code == '403'
+ response.code == '201' ? 0 : 1
+ end
+
+ private
+
+ def debug(msg)
+ puts msg if verbose
+ end
+end
+
+handler = RedmineMailHandler.new
+handler.submit(STDIN.read)
--- /dev/null
+== Sample plugin
+
+This is a sample plugin for Redmine
+
+== Installation
+
+1. Copy the plugin directory into the vendor/plugins directory
+
+2. Migrate plugin:
+ rake db:migrate_plugins
+
+3. Start Redmine
+
+Installed plugins are listed and can be configured from 'Admin -> Plugins' screen.
--- /dev/null
+# Sample plugin controller
+class ExampleController < ApplicationController
+ unloadable
+
+ layout 'base'
+ before_filter :find_project, :authorize
+ menu_item :sample_plugin
+
+ def say_hello
+ @value = Setting.plugin_sample_plugin['sample_setting']
+ end
+
+ def say_goodbye
+ end
+
+private
+ def find_project
+ @project=Project.find(params[:id])
+ end
+end
--- /dev/null
+class Meeting < ActiveRecord::Base
+ belongs_to :project
+
+ acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
+ :datetime => :scheduled_on,
+ :author => nil,
+ :url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
+
+ acts_as_activity_provider :timestamp => 'scheduled_on',
+ :find_options => { :include => :project }
+end
--- /dev/null
+<p class="icon icon-example-works"><%= l(:text_say_goodbye) %></p>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag "example.css", :plugin => "sample_plugin", :media => "screen" %>
+<% end %>
--- /dev/null
+<p class="icon icon-example-works"><%= l(:text_say_hello) %></p>
+
+<p><label>Example setting</label>: <%= @value %></p>
+
+<%= link_to_if_authorized 'Good bye', :action => 'say_goodbye', :id => @project %>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag "example.css", :plugin => "sample_plugin", :media => "screen" %>
+<% end %>
--- /dev/null
+<h3>Sample block</h3>
+
+You are <strong><%= h(User.current) %></strong> and this is a sample block for My Page added from a plugin.
--- /dev/null
+<p><label>Example setting</label><%= text_field_tag 'settings[sample_setting]', @settings['sample_setting'] %></p>
+
+<p><label>Foo</label><%= text_field_tag 'settings[foo]', @settings['foo'] %></p>
--- /dev/null
+.icon-example-works { background-image: url(../images/it_works.png); }
--- /dev/null
+# Sample plugin
+en:
+ label_plugin_example: Sample Plugin
+ label_meeting_plural: Meetings
+ text_say_hello: Plugin say 'Hello'
+ text_say_goodbye: Plugin say 'Good bye'
--- /dev/null
+# Sample plugin
+fr:
+ label_plugin_example: Plugin exemple
+ label_meeting_plural: Meetings
+ text_say_hello: Plugin dit 'Bonjour'
+ text_say_goodbye: Plugin dit 'Au revoir'
--- /dev/null
+# Sample plugin migration
+# Use rake db:migrate_plugins to migrate installed plugins
+class CreateMeetings < ActiveRecord::Migration
+ def self.up
+ create_table :meetings do |t|
+ t.column :project_id, :integer, :null => false
+ t.column :description, :string
+ t.column :scheduled_on, :datetime
+ end
+ end
+
+ def self.down
+ drop_table :meetings
+ end
+end
--- /dev/null
+# Redmine sample plugin
+require 'redmine'
+
+RAILS_DEFAULT_LOGGER.info 'Starting Example plugin for RedMine'
+
+Redmine::Plugin.register :sample_plugin do
+ name 'Example plugin'
+ author 'Author name'
+ description 'This is a sample plugin for Redmine'
+ version '0.0.1'
+ settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'settings/sample_plugin_settings'
+
+ # This plugin adds a project module
+ # It can be enabled/disabled at project level (Project settings -> Modules)
+ project_module :example_module do
+ # A public action
+ permission :example_say_hello, {:example => [:say_hello]}, :public => true
+ # This permission has to be explicitly given
+ # It will be listed on the permissions screen
+ permission :example_say_goodbye, {:example => [:say_goodbye]}
+ # This permission can be given to project members only
+ permission :view_meetings, {:meetings => [:index, :show]}, :require => :member
+ end
+
+ # A new item is added to the project menu
+ menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
+
+ # Meetings are added to the activity view
+ activity_provider :meetings
+end
--- /dev/null
+package Apache::Authn::Redmine;
+
+=head1 Apache::Authn::Redmine
+
+Redmine - a mod_perl module to authenticate webdav subversion users
+against redmine database
+
+=head1 SYNOPSIS
+
+This module allow anonymous users to browse public project and
+registred users to browse and commit their project. Authentication is
+done against the redmine database or the LDAP configured in redmine.
+
+This method is far simpler than the one with pam_* and works with all
+database without an hassle but you need to have apache/mod_perl on the
+svn server.
+
+=head1 INSTALLATION
+
+For this to automagically work, you need to have a recent reposman.rb
+(after r860) and if you already use reposman, read the last section to
+migrate.
+
+Sorry ruby users but you need some perl modules, at least mod_perl2,
+DBI and DBD::mysql (or the DBD driver for you database as it should
+work on allmost all databases).
+
+On debian/ubuntu you must do :
+
+ aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
+
+If your Redmine users use LDAP authentication, you will also need
+Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
+
+ aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
+
+=head1 CONFIGURATION
+
+ ## This module has to be in your perl path
+ ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
+ PerlLoadModule Apache::Authn::Redmine
+ <Location /svn>
+ DAV svn
+ SVNParentPath "/var/svn"
+
+ AuthType Basic
+ AuthName redmine
+ Require valid-user
+
+ PerlAccessHandler Apache::Authn::Redmine::access_handler
+ PerlAuthenHandler Apache::Authn::Redmine::authen_handler
+
+ ## for mysql
+ RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
+ ## for postgres
+ # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
+
+ RedmineDbUser "redmine"
+ RedmineDbPass "password"
+ ## Optional where clause (fulltext search would be slow and
+ ## database dependant).
+ # RedmineDbWhereClause "and members.role_id IN (1,2)"
+ ## Optional credentials cache size
+ # RedmineCacheCredsMax 50
+ </Location>
+
+To be able to browse repository inside redmine, you must add something
+like that :
+
+ <Location /svn-private>
+ DAV svn
+ SVNParentPath "/var/svn"
+ Order deny,allow
+ Deny from all
+ # only allow reading orders
+ <Limit GET PROPFIND OPTIONS REPORT>
+ Allow from redmine.server.ip
+ </Limit>
+ </Location>
+
+and you will have to use this reposman.rb command line to create repository :
+
+ reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
+
+=head1 MIGRATION FROM OLDER RELEASES
+
+If you use an older reposman.rb (r860 or before), you need to change
+rights on repositories to allow the apache user to read and write
+S<them :>
+
+ sudo chown -R www-data /var/svn/*
+ sudo chmod -R u+w /var/svn/*
+
+And you need to upgrade at least reposman.rb (after r860).
+
+=cut
+
+use strict;
+use warnings FATAL => 'all', NONFATAL => 'redefine';
+
+use DBI;
+use Digest::SHA1;
+# optional module for LDAP authentication
+my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
+
+use Apache2::Module;
+use Apache2::Access;
+use Apache2::ServerRec qw();
+use Apache2::RequestRec qw();
+use Apache2::RequestUtil qw();
+use Apache2::Const qw(:common :override :cmd_how);
+use APR::Pool ();
+use APR::Table ();
+
+# use Apache2::Directive qw();
+
+my @directives = (
+ {
+ name => 'RedmineDSN',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
+ },
+ {
+ name => 'RedmineDbUser',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineDbPass',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineDbWhereClause',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineCacheCredsMax',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ errmsg => 'RedmineCacheCredsMax must be decimal number',
+ },
+);
+
+sub RedmineDSN {
+ my ($self, $parms, $arg) = @_;
+ $self->{RedmineDSN} = $arg;
+ my $query = "SELECT
+ hashed_password, auth_source_id, permissions
+ FROM members, projects, users, roles, member_roles
+ WHERE
+ projects.id=members.project_id
+ AND member_roles.member_id=members.id
+ AND users.id=members.user_id
+ AND roles.id=member_roles.role_id
+ AND users.status=1
+ AND login=?
+ AND identifier=? ";
+ $self->{RedmineQuery} = trim($query);
+}
+
+sub RedmineDbUser { set_val('RedmineDbUser', @_); }
+sub RedmineDbPass { set_val('RedmineDbPass', @_); }
+sub RedmineDbWhereClause {
+ my ($self, $parms, $arg) = @_;
+ $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
+}
+
+sub RedmineCacheCredsMax {
+ my ($self, $parms, $arg) = @_;
+ if ($arg) {
+ $self->{RedmineCachePool} = APR::Pool->new;
+ $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
+ $self->{RedmineCacheCredsCount} = 0;
+ $self->{RedmineCacheCredsMax} = $arg;
+ }
+}
+
+sub trim {
+ my $string = shift;
+ $string =~ s/\s{2,}/ /g;
+ return $string;
+}
+
+sub set_val {
+ my ($key, $self, $parms, $arg) = @_;
+ $self->{$key} = $arg;
+}
+
+Apache2::Module::add(__PACKAGE__, \@directives);
+
+
+my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
+
+sub access_handler {
+ my $r = shift;
+
+ unless ($r->some_auth_required) {
+ $r->log_reason("No authentication has been configured");
+ return FORBIDDEN;
+ }
+
+ my $method = $r->method;
+ return OK unless defined $read_only_methods{$method};
+
+ my $project_id = get_project_identifier($r);
+
+ $r->set_handlers(PerlAuthenHandler => [\&OK])
+ if is_public_project($project_id, $r);
+
+ return OK
+}
+
+sub authen_handler {
+ my $r = shift;
+
+ my ($res, $redmine_pass) = $r->get_basic_auth_pw();
+ return $res unless $res == OK;
+
+ if (is_member($r->user, $redmine_pass, $r)) {
+ return OK;
+ } else {
+ $r->note_auth_failure();
+ return AUTH_REQUIRED;
+ }
+}
+
+sub is_public_project {
+ my $project_id = shift;
+ my $r = shift;
+
+ my $dbh = connect_database($r);
+ my $sth = $dbh->prepare(
+ "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
+ );
+
+ $sth->execute($project_id);
+ my $ret = $sth->fetchrow_array ? 1 : 0;
+ $sth->finish();
+ $dbh->disconnect();
+
+ $ret;
+}
+
+# perhaps we should use repository right (other read right) to check public access.
+# it could be faster BUT it doesn't work for the moment.
+# sub is_public_project_by_file {
+# my $project_id = shift;
+# my $r = shift;
+
+# my $tree = Apache2::Directive::conftree();
+# my $node = $tree->lookup('Location', $r->location);
+# my $hash = $node->as_hash;
+
+# my $svnparentpath = $hash->{SVNParentPath};
+# my $repos_path = $svnparentpath . "/" . $project_id;
+# return 1 if (stat($repos_path))[2] & 00007;
+# }
+
+sub is_member {
+ my $redmine_user = shift;
+ my $redmine_pass = shift;
+ my $r = shift;
+
+ my $dbh = connect_database($r);
+ my $project_id = get_project_identifier($r);
+
+ my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
+
+ my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
+ my $usrprojpass;
+ if ($cfg->{RedmineCacheCredsMax}) {
+ $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
+ return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
+ }
+ my $query = $cfg->{RedmineQuery};
+ my $sth = $dbh->prepare($query);
+ $sth->execute($redmine_user, $project_id);
+
+ my $ret;
+ while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
+
+ unless ($auth_source_id) {
+ my $method = $r->method;
+ if ($hashed_password eq $pass_digest && (defined $read_only_methods{$method} || $permissions =~ /:commit_access/) ) {
+ $ret = 1;
+ last;
+ }
+ } elsif ($CanUseLDAPAuth) {
+ my $sthldap = $dbh->prepare(
+ "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
+ );
+ $sthldap->execute($auth_source_id);
+ while (my @rowldap = $sthldap->fetchrow_array) {
+ my $ldap = Authen::Simple::LDAP->new(
+ host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
+ port => $rowldap[1],
+ basedn => $rowldap[5],
+ binddn => $rowldap[3] ? $rowldap[3] : "",
+ bindpw => $rowldap[4] ? $rowldap[4] : "",
+ filter => "(".$rowldap[6]."=%s)"
+ );
+ $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass));
+ }
+ $sthldap->finish();
+ }
+ }
+ $sth->finish();
+ $dbh->disconnect();
+
+ if ($cfg->{RedmineCacheCredsMax} and $ret) {
+ if (defined $usrprojpass) {
+ $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
+ } else {
+ if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
+ $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
+ $cfg->{RedmineCacheCredsCount}++;
+ } else {
+ $cfg->{RedmineCacheCreds}->clear();
+ $cfg->{RedmineCacheCredsCount} = 0;
+ }
+ }
+ }
+
+ $ret;
+}
+
+sub get_project_identifier {
+ my $r = shift;
+
+ my $location = $r->location;
+ my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
+ $identifier;
+}
+
+sub connect_database {
+ my $r = shift;
+
+ my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
+ return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
+}
+
+1;
--- /dev/null
+/* ssh views */
+
+CREATE OR REPLACE VIEW ssh_users as
+select login as username, hashed_password as password
+from users
+where status = 1;
+
+
+/* nss views */
+
+CREATE OR REPLACE VIEW nss_groups AS
+select identifier AS name, (id + 5000) AS gid, 'x' AS password
+from projects;
+
+CREATE OR REPLACE VIEW nss_users AS
+select login AS username, CONCAT_WS(' ', firstname, lastname) as realname, (id + 5000) AS uid, 'x' AS password
+from users
+where status = 1;
+
+CREATE OR REPLACE VIEW nss_grouplist AS
+select (members.project_id + 5000) AS gid, users.login AS username
+from users, members
+where users.id = members.user_id
+and users.status = 1;
--- /dev/null
+#!/usr/bin/env ruby
+
+# == Synopsis
+#
+# reposman: manages your repositories with Redmine
+#
+# == Usage
+#
+# reposman [OPTIONS...] -s [DIR] -r [HOST]
+#
+# Examples:
+# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion
+# reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git
+#
+# == Arguments (mandatory)
+#
+# -s, --svn-dir=DIR use DIR as base directory for svn repositories
+# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples:
+# -r redmine.example.net
+# -r http://redmine.example.net
+# -r https://example.net/redmine
+#
+# == Options
+#
+# -o, --owner=OWNER owner of the repository. using the rails login
+# allow user to browse the repository within
+# Redmine even for private project. If you want to share repositories
+# through Redmine.pm, you need to use the apache owner.
+# -g, --group=GROUP group of the repository. (default: root)
+# --scm=SCM the kind of SCM repository you want to create (and register) in
+# Redmine (default: Subversion). reposman is able to create Git
+# and Subversion repositories. For all other kind (Bazaar,
+# Darcs, Filesystem, Mercurial) you must specify a --command option
+# -u, --url=URL the base url Redmine will use to access your
+# repositories. This option is used to automatically
+# register the repositories in Redmine. The project
+# identifier will be appended to this url. Examples:
+# -u https://example.net/svn
+# -u file:///var/svn/
+# if this option isn't set, reposman won't register
+# the repositories in Redmine
+# -c, --command=COMMAND use this command instead of "svnadmin create" to
+# create a repository. This option can be used to
+# create repositories other than subversion and git kind.
+# This command override the default creation for git and subversion.
+# -f, --force force repository creation even if the project
+# repository is already declared in Redmine
+# -t, --test only show what should be done
+# -h, --help show help and exit
+# -v, --verbose verbose
+# -V, --version print version and exit
+# -q, --quiet no log
+#
+# == References
+#
+# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos
+
+
+require 'getoptlong'
+require 'rdoc/usage'
+require 'find'
+require 'etc'
+
+Version = "1.3"
+SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem )
+
+opts = GetoptLong.new(
+ ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
+ ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
+ ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT],
+ ['--group', '-g', GetoptLong::REQUIRED_ARGUMENT],
+ ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
+ ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT],
+ ['--scm', GetoptLong::REQUIRED_ARGUMENT],
+ ['--test', '-t', GetoptLong::NO_ARGUMENT],
+ ['--force', '-f', GetoptLong::NO_ARGUMENT],
+ ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
+ ['--version', '-V', GetoptLong::NO_ARGUMENT],
+ ['--help' , '-h', GetoptLong::NO_ARGUMENT],
+ ['--quiet' , '-q', GetoptLong::NO_ARGUMENT]
+ )
+
+$verbose = 0
+$quiet = false
+$redmine_host = ''
+$repos_base = ''
+$svn_owner = 'root'
+$svn_group = 'root'
+$use_groupid = true
+$svn_url = false
+$test = false
+$force = false
+$scm = 'Subversion'
+
+def log(text, options={})
+ level = options[:level] || 0
+ puts text unless $quiet or level > $verbose
+ exit 1 if options[:exit]
+end
+
+def system_or_raise(command)
+ raise "\"#{command}\" failed" unless system command
+end
+
+module SCM
+
+ module Subversion
+ def self.create(path)
+ system_or_raise "svnadmin create #{path}"
+ end
+ end
+
+ module Git
+ def self.create(path)
+ Dir.mkdir path
+ Dir.chdir(path) do
+ system_or_raise "git --bare init --shared"
+ system_or_raise "git update-server-info"
+ end
+ end
+ end
+
+end
+
+begin
+ opts.each do |opt, arg|
+ case opt
+ when '--svn-dir'; $repos_base = arg.dup
+ when '--redmine-host'; $redmine_host = arg.dup
+ when '--owner'; $svn_owner = arg.dup; $use_groupid = false;
+ when '--group'; $svn_group = arg.dup; $use_groupid = false;
+ when '--url'; $svn_url = arg.dup
+ when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm)
+ when '--command'; $command = arg.dup
+ when '--verbose'; $verbose += 1
+ when '--test'; $test = true
+ when '--force'; $force = true
+ when '--version'; puts Version; exit
+ when '--help'; RDoc::usage
+ when '--quiet'; $quiet = true
+ end
+ end
+rescue
+ exit 1
+end
+
+if $test
+ log("running in test mode")
+end
+
+# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git)
+if $command.nil?
+ begin
+ scm_module = SCM.const_get($scm)
+ rescue
+ log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true)
+ end
+end
+
+$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
+
+if ($redmine_host.empty? or $repos_base.empty?)
+ RDoc::usage
+end
+
+unless File.directory?($repos_base)
+ log("directory '#{$repos_base}' doesn't exists", :exit => true)
+end
+
+begin
+ require 'activeresource'
+rescue LoadError
+ log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
+end
+
+class Project < ActiveResource::Base; end
+
+log("querying Redmine for projects...", :level => 1);
+
+$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
+$redmine_host.gsub!(/\/$/, '')
+
+Project.site = "#{$redmine_host}/sys";
+
+begin
+ # Get all active projects that have the Repository module enabled
+ projects = Project.find(:all)
+rescue => e
+ log("Unable to connect to #{Project.site}: #{e}", :exit => true)
+end
+
+if projects.nil?
+ log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
+end
+
+log("retrieved #{projects.size} projects", :level => 1)
+
+def set_owner_and_rights(project, repos_path, &block)
+ if RUBY_PLATFORM =~ /mswin/
+ yield if block_given?
+ else
+ uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid)
+ right = project.is_public ? 0775 : 0770
+ yield if block_given?
+ Find.find(repos_path) do |f|
+ File.chmod right, f
+ File.chown uid, gid, f
+ end
+ end
+end
+
+def other_read_right?(file)
+ (File.stat(file).mode & 0007).zero? ? false : true
+end
+
+def owner_name(file)
+ RUBY_PLATFORM =~ /mswin/ ?
+ $svn_owner :
+ Etc.getpwuid( File.stat(file).uid ).name
+end
+
+projects.each do |project|
+ log("treating project #{project.name}", :level => 1)
+
+ if project.identifier.empty?
+ log("\tno identifier for project #{project.name}")
+ next
+ elsif not project.identifier.match(/^[a-z0-9\-]+$/)
+ log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
+ next;
+ end
+
+ repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
+
+ if File.directory?(repos_path)
+
+ # we must verify that repository has the good owner and the good
+ # rights before leaving
+ other_read = other_read_right?(repos_path)
+ owner = owner_name(repos_path)
+ next if project.is_public == other_read and owner == $svn_owner
+
+ if $test
+ log("\tchange mode on #{repos_path}")
+ next
+ end
+
+ begin
+ set_owner_and_rights(project, repos_path)
+ rescue Errno::EPERM => e
+ log("\tunable to change mode on #{repos_path} : #{e}\n")
+ next
+ end
+
+ log("\tmode change on #{repos_path}");
+
+ else
+ # if repository is already declared in redmine, we don't create
+ # unless user use -f with reposman
+ if $force == false and project.respond_to?(:repository)
+ log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1)
+ next
+ end
+
+ project.is_public ? File.umask(0002) : File.umask(0007)
+
+ if $test
+ log("\tcreate repository #{repos_path}")
+ log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url;
+ next
+ end
+
+ begin
+ set_owner_and_rights(project, repos_path) do
+ if scm_module.nil?
+ system_or_raise "#{$command} #{repos_path}"
+ else
+ scm_module.create(repos_path)
+ end
+ end
+ rescue => e
+ log("\tunable to create #{repos_path} : #{e}\n")
+ next
+ end
+
+ if $svn_url
+ begin
+ project.post(:repository, :vendor => $scm, :repository => {:url => "#{$svn_url}#{project.identifier}"})
+ log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
+ rescue => e
+ log("\trepository #{repos_path} not registered in Redmine: #{e.message}");
+ end
+ end
+
+ log("\trepository #{repos_path} created");
+ end
+
+end
+
--- /dev/null
+#!/usr/bin/perl
+#
+# redMine is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# modify to suit your repository base
+my $repos_base = '/var/svn';
+
+my $path = '/usr/bin/';
+my %kwown_commands = map { $_ => 1 } qw/svnserve/;
+
+umask 0002;
+
+exec ('/usr/bin/svnserve', '-r', $repos_base, '-t');
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+\f
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
--- /dev/null
+require 'rexml/document'\r
+require 'SVG/Graph/Graph'\r
+require 'SVG/Graph/BarBase'\r
+\r
+module SVG\r
+ module Graph\r
+ # === Create presentation quality SVG bar graphs easily\r
+ #\r
+ # = Synopsis\r
+ #\r
+ # require 'SVG/Graph/Bar'\r
+ #\r
+ # fields = %w(Jan Feb Mar);\r
+ # data_sales_02 = [12, 45, 21]\r
+ #\r
+ # graph = SVG::Graph::Bar.new(\r
+ # :height => 500,\r
+ # :width => 300,\r
+ # :fields => fields\r
+ # )\r
+ #\r
+ # graph.add_data(\r
+ # :data => data_sales_02,\r
+ # :title => 'Sales 2002'\r
+ # )\r
+ #\r
+ # print "Content-type: image/svg+xml\r\n\r\n"\r
+ # print graph.burn\r
+ #\r
+ # = Description\r
+ #\r
+ # This object aims to allow you to easily create high quality\r
+ # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default\r
+ # style sheet or supply your own. Either way there are many options which\r
+ # can be configured to give you control over how the graph is generated -\r
+ # with or without a key, data elements at each point, title, subtitle etc.\r
+ #\r
+ # = Notes\r
+ #\r
+ # The default stylesheet handles upto 12 data sets, if you\r
+ # use more you must create your own stylesheet and add the\r
+ # additional settings for the extra data sets. You will know\r
+ # if you go over 12 data sets as they will have no style and\r
+ # be in black.\r
+ #\r
+ # = Examples\r
+ #\r
+ # * http://germane-software.com/repositories/public/SVG/test/test.rb\r
+ #\r
+ # = See also\r
+ #\r
+ # * SVG::Graph::Graph\r
+ # * SVG::Graph::BarHorizontal\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Pie\r
+ # * SVG::Graph::Plot\r
+ # * SVG::Graph::TimeSeries\r
+ class Bar < BarBase\r
+ include REXML\r
+\r
+ # See Graph::initialize and BarBase::set_defaults\r
+ def set_defaults \r
+ super\r
+ self.top_align = self.top_font = 1\r
+ end\r
+\r
+ protected\r
+\r
+ def get_x_labels\r
+ @config[:fields]\r
+ end\r
+\r
+ def get_y_labels\r
+ maxvalue = max_value\r
+ minvalue = min_value\r
+ range = maxvalue - minvalue\r
+\r
+ top_pad = range == 0 ? 10 : range / 20.0\r
+ scale_range = (maxvalue + top_pad) - minvalue\r
+\r
+ scale_division = scale_divisions || (scale_range / 10.0)\r
+\r
+ if scale_integers\r
+ scale_division = scale_division < 1 ? 1 : scale_division.round\r
+ end\r
+\r
+ rv = []\r
+ maxvalue = maxvalue%scale_division == 0 ? \r
+ maxvalue : maxvalue + scale_division\r
+ minvalue.step( maxvalue, scale_division ) {|v| rv << v}\r
+ return rv\r
+ end\r
+\r
+ def x_label_offset( width )\r
+ width / 2.0\r
+ end\r
+\r
+ def draw_data\r
+ minvalue = min_value\r
+ fieldwidth = field_width\r
+\r
+ unit_size = (@graph_height.to_f - font_size*2*top_font) / \r
+ (get_y_labels.max - get_y_labels.min)\r
+ bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0\r
+\r
+ bar_width = fieldwidth - bargap\r
+ bar_width /= @data.length if stack == :side\r
+ x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)\r
+ \r
+ bottom = @graph_height\r
+\r
+ field_count = 0\r
+ @config[:fields].each_index { |i|\r
+ dataset_count = 0\r
+ for dataset in @data\r
+ \r
+ # cases (assume 0 = +ve):\r
+ # value min length\r
+ # +ve +ve value - min\r
+ # +ve -ve value - 0\r
+ # -ve -ve value.abs - 0\r
+ \r
+ value = dataset[:data][i]\r
+ \r
+ left = (fieldwidth * field_count)\r
+ \r
+ length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size\r
+ # top is 0 if value is negative\r
+ top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)\r
+ left += bar_width * dataset_count if stack == :side\r
+ \r
+ @graph.add_element( "rect", {\r
+ "x" => left.to_s,\r
+ "y" => top.to_s,\r
+ "width" => bar_width.to_s,\r
+ "height" => length.to_s,\r
+ "class" => "fill#{dataset_count+1}"\r
+ })\r
+\r
+ make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)\r
+ dataset_count += 1\r
+ end\r
+ field_count += 1\r
+ }\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'rexml/document'\r
+require 'SVG/Graph/Graph'\r
+\r
+module SVG\r
+ module Graph\r
+ # = Synopsis\r
+ #\r
+ # A superclass for bar-style graphs. Do not attempt to instantiate\r
+ # directly; use one of the subclasses instead.\r
+ #\r
+ # = Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class BarBase < SVG::Graph::Graph\r
+ # Ensures that :fields are provided in the configuration.\r
+ def initialize config\r
+ raise "fields was not supplied or is empty" unless config[:fields] &&\r
+ config[:fields].kind_of?(Array) &&\r
+ config[:fields].length > 0\r
+ super\r
+ end\r
+\r
+ # In addition to the defaults set in Graph::initialize, sets\r
+ # [bar_gap] true\r
+ # [stack] :overlap\r
+ def set_defaults\r
+ init_with( :bar_gap => true, :stack => :overlap )\r
+ end\r
+\r
+ # Whether to have a gap between the bars or not, default\r
+ # is true, set to false if you don't want gaps.\r
+ attr_accessor :bar_gap\r
+ # How to stack data sets. :overlap overlaps bars with\r
+ # transparent colors, :top stacks bars on top of one another,\r
+ # :side stacks the bars side-by-side. Defaults to :overlap.\r
+ attr_accessor :stack\r
+\r
+\r
+ protected\r
+\r
+ def max_value\r
+ @data.collect{|x| x[:data].max}.max\r
+ end\r
+\r
+ def min_value\r
+ min = 0\r
+ if min_scale_value.nil? \r
+ min = @data.collect{|x| x[:data].min}.min\r
+ min = min > 0 ? 0 : min\r
+ else\r
+ min = min_scale_value\r
+ end\r
+ return min\r
+ end\r
+\r
+ def get_css\r
+ return <<EOL\r
+/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */\r
+.key1,.fill1{\r
+ fill: #ff0000;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 0.5px; \r
+}\r
+.key2,.fill2{\r
+ fill: #0000ff;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key3,.fill3{\r
+ fill: #00ff00;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key4,.fill4{\r
+ fill: #ffcc00;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key5,.fill5{\r
+ fill: #00ccff;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key6,.fill6{\r
+ fill: #ff00ff;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key7,.fill7{\r
+ fill: #00ffff;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key8,.fill8{\r
+ fill: #ffff00;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key9,.fill9{\r
+ fill: #cc6666;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key10,.fill10{\r
+ fill: #663399;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key11,.fill11{\r
+ fill: #339900;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key12,.fill12{\r
+ fill: #9966FF;\r
+ fill-opacity: 0.5;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+EOL\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'rexml/document'\r
+require 'SVG/Graph/BarBase'\r
+\r
+module SVG\r
+ module Graph\r
+ # === Create presentation quality SVG horitonzal bar graphs easily\r
+ # \r
+ # = Synopsis\r
+ # \r
+ # require 'SVG/Graph/BarHorizontal'\r
+ # \r
+ # fields = %w(Jan Feb Mar)\r
+ # data_sales_02 = [12, 45, 21]\r
+ # \r
+ # graph = SVG::Graph::BarHorizontal.new({\r
+ # :height => 500,\r
+ # :width => 300,\r
+ # :fields => fields,\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => data_sales_02,\r
+ # :title => 'Sales 2002',\r
+ # })\r
+ # \r
+ # print "Content-type: image/svg+xml\r\n\r\n"\r
+ # print graph.burn\r
+ # \r
+ # = Description\r
+ # \r
+ # This object aims to allow you to easily create high quality\r
+ # SVG horitonzal bar graphs. You can either use the default style sheet\r
+ # or supply your own. Either way there are many options which can\r
+ # be configured to give you control over how the graph is\r
+ # generated - with or without a key, data elements at each point,\r
+ # title, subtitle etc.\r
+ # \r
+ # = Examples\r
+ # \r
+ # * http://germane-software.com/repositories/public/SVG/test/test.rb\r
+ # \r
+ # = See also\r
+ # \r
+ # * SVG::Graph::Graph\r
+ # * SVG::Graph::Bar\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Pie\r
+ # * SVG::Graph::Plot\r
+ # * SVG::Graph::TimeSeries\r
+ #\r
+ # == Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class BarHorizontal < BarBase\r
+ # In addition to the defaults set in BarBase::set_defaults, sets\r
+ # [rotate_y_labels] true\r
+ # [show_x_guidelines] true\r
+ # [show_y_guidelines] false\r
+ def set_defaults\r
+ super\r
+ init_with( \r
+ :rotate_y_labels => true,\r
+ :show_x_guidelines => true,\r
+ :show_y_guidelines => false\r
+ )\r
+ self.right_align = self.right_font = 1\r
+ end\r
+ \r
+ protected\r
+\r
+ def get_x_labels\r
+ maxvalue = max_value\r
+ minvalue = min_value\r
+ range = maxvalue - minvalue\r
+ top_pad = range == 0 ? 10 : range / 20.0\r
+ scale_range = (maxvalue + top_pad) - minvalue\r
+\r
+ scale_division = scale_divisions || (scale_range / 10.0)\r
+\r
+ if scale_integers\r
+ scale_division = scale_division < 1 ? 1 : scale_division.round\r
+ end\r
+\r
+ rv = []\r
+ maxvalue = maxvalue%scale_division == 0 ? \r
+ maxvalue : maxvalue + scale_division\r
+ minvalue.step( maxvalue, scale_division ) {|v| rv << v}\r
+ return rv\r
+ end\r
+\r
+ def get_y_labels\r
+ @config[:fields]\r
+ end\r
+\r
+ def y_label_offset( height )\r
+ height / -2.0\r
+ end\r
+\r
+ def draw_data\r
+ minvalue = min_value\r
+ fieldheight = field_height\r
+\r
+ unit_size = (@graph_width.to_f - font_size*2*right_font ) /\r
+ (get_x_labels.max - get_x_labels.min )\r
+ bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0\r
+\r
+ bar_height = fieldheight - bargap\r
+ bar_height /= @data.length if stack == :side\r
+ y_mod = (bar_height / 2) + (font_size / 2)\r
+ \r
+ field_count = 1\r
+ @config[:fields].each_index { |i|\r
+ dataset_count = 0\r
+ for dataset in @data\r
+ value = dataset[:data][i]\r
+ \r
+ top = @graph_height - (fieldheight * field_count)\r
+ top += (bar_height * dataset_count) if stack == :side\r
+ # cases (assume 0 = +ve):\r
+ # value min length left\r
+ # +ve +ve value.abs - min minvalue.abs\r
+ # +ve -ve value.abs - 0 minvalue.abs\r
+ # -ve -ve value.abs - 0 minvalue.abs + value\r
+ length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size\r
+ left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size\r
+\r
+ @graph.add_element( "rect", {\r
+ "x" => left.to_s,\r
+ "y" => top.to_s,\r
+ "width" => length.to_s,\r
+ "height" => bar_height.to_s,\r
+ "class" => "fill#{dataset_count+1}"\r
+ })\r
+\r
+ make_datapoint_text( \r
+ left+length+5, top+y_mod, value, "text-anchor: start; "\r
+ )\r
+ dataset_count += 1\r
+ end\r
+ field_count += 1\r
+ }\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+begin\r
+ require 'zlib'\r
+ @@__have_zlib = true\r
+rescue\r
+ @@__have_zlib = false\r
+end\r
+\r
+require 'rexml/document'\r
+\r
+module SVG\r
+ module Graph\r
+ VERSION = '@ANT_VERSION@'\r
+\r
+ # === Base object for generating SVG Graphs\r
+ # \r
+ # == Synopsis\r
+ #\r
+ # This class is only used as a superclass of specialized charts. Do not\r
+ # attempt to use this class directly, unless creating a new chart type.\r
+ #\r
+ # For examples of how to subclass this class, see the existing specific\r
+ # subclasses, such as SVG::Graph::Pie.\r
+ #\r
+ # == Examples\r
+ #\r
+ # For examples of how to use this package, see either the test files, or\r
+ # the documentation for the specific class you want to use.\r
+ #\r
+ # * file:test/plot.rb\r
+ # * file:test/single.rb\r
+ # * file:test/test.rb\r
+ # * file:test/timeseries.rb\r
+ # \r
+ # == Description\r
+ # \r
+ # This package should be used as a base for creating SVG graphs.\r
+ #\r
+ # == Acknowledgements\r
+ #\r
+ # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby\r
+ # port is based on.\r
+ #\r
+ # Stephen Morgan for creating the TT template and SVG.\r
+ # \r
+ # == See\r
+ #\r
+ # * SVG::Graph::BarHorizontal\r
+ # * SVG::Graph::Bar\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Pie\r
+ # * SVG::Graph::Plot\r
+ # * SVG::Graph::TimeSeries\r
+ #\r
+ # == Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class Graph\r
+ include REXML\r
+\r
+ # Initialize the graph object with the graph settings. You won't\r
+ # instantiate this class directly; see the subclass for options.\r
+ # [width] 500\r
+ # [height] 300\r
+ # [show_x_guidelines] false\r
+ # [show_y_guidelines] true\r
+ # [show_data_values] true\r
+ # [min_scale_value] 0\r
+ # [show_x_labels] true\r
+ # [stagger_x_labels] false\r
+ # [rotate_x_labels] false\r
+ # [step_x_labels] 1\r
+ # [step_include_first_x_label] true\r
+ # [show_y_labels] true\r
+ # [rotate_y_labels] false\r
+ # [scale_integers] false\r
+ # [show_x_title] false\r
+ # [x_title] 'X Field names'\r
+ # [show_y_title] false\r
+ # [y_title_text_direction] :bt\r
+ # [y_title] 'Y Scale'\r
+ # [show_graph_title] false\r
+ # [graph_title] 'Graph Title'\r
+ # [show_graph_subtitle] false\r
+ # [graph_subtitle] 'Graph Sub Title'\r
+ # [key] true,\r
+ # [key_position] :right, # bottom or righ\r
+ # [font_size] 12\r
+ # [title_font_size] 16\r
+ # [subtitle_font_size] 14\r
+ # [x_label_font_size] 12\r
+ # [x_title_font_size] 14\r
+ # [y_label_font_size] 12\r
+ # [y_title_font_size] 14\r
+ # [key_font_size] 10\r
+ # [no_css] false\r
+ # [add_popups] false\r
+ def initialize( config )\r
+ @config = config\r
+\r
+ self.top_align = self.top_font = self.right_align = self.right_font = 0\r
+\r
+ init_with({\r
+ :width => 500,\r
+ :height => 300,\r
+ :show_x_guidelines => false,\r
+ :show_y_guidelines => true,\r
+ :show_data_values => true,\r
+\r
+# :min_scale_value => 0,\r
+\r
+ :show_x_labels => true,\r
+ :stagger_x_labels => false,\r
+ :rotate_x_labels => false,\r
+ :step_x_labels => 1,\r
+ :step_include_first_x_label => true,\r
+\r
+ :show_y_labels => true,\r
+ :rotate_y_labels => false,\r
+ :stagger_y_labels => false,\r
+ :scale_integers => false,\r
+\r
+ :show_x_title => false,\r
+ :x_title => 'X Field names',\r
+\r
+ :show_y_title => false,\r
+ :y_title_text_direction => :bt,\r
+ :y_title => 'Y Scale',\r
+\r
+ :show_graph_title => false,\r
+ :graph_title => 'Graph Title',\r
+ :show_graph_subtitle => false,\r
+ :graph_subtitle => 'Graph Sub Title',\r
+ :key => true, \r
+ :key_position => :right, # bottom or right\r
+\r
+ :font_size =>12,\r
+ :title_font_size =>16,\r
+ :subtitle_font_size =>14,\r
+ :x_label_font_size =>12,\r
+ :x_title_font_size =>14,\r
+ :y_label_font_size =>12,\r
+ :y_title_font_size =>14,\r
+ :key_font_size =>10,\r
+ \r
+ :no_css =>false,\r
+ :add_popups =>false,\r
+ })\r
+\r
+ set_defaults if respond_to? :set_defaults\r
+\r
+ init_with config\r
+ end\r
+\r
+ \r
+ # This method allows you do add data to the graph object.\r
+ # It can be called several times to add more data sets in.\r
+ #\r
+ # data_sales_02 = [12, 45, 21];\r
+ # \r
+ # graph.add_data({\r
+ # :data => data_sales_02,\r
+ # :title => 'Sales 2002'\r
+ # })\r
+ def add_data conf\r
+ @data = [] unless defined? @data\r
+\r
+ if conf[:data] and conf[:data].kind_of? Array\r
+ @data << conf\r
+ else\r
+ raise "No data provided by #{conf.inspect}"\r
+ end\r
+ end\r
+\r
+\r
+ # This method removes all data from the object so that you can\r
+ # reuse it to create a new graph but with the same config options.\r
+ #\r
+ # graph.clear_data\r
+ def clear_data \r
+ @data = []\r
+ end\r
+\r
+\r
+ # This method processes the template with the data and\r
+ # config which has been set and returns the resulting SVG.\r
+ #\r
+ # This method will croak unless at least one data set has\r
+ # been added to the graph object.\r
+ #\r
+ # print graph.burn\r
+ def burn\r
+ raise "No data available" unless @data.size > 0\r
+ \r
+ calculations if respond_to? :calculations\r
+\r
+ start_svg\r
+ calculate_graph_dimensions\r
+ @foreground = Element.new( "g" )\r
+ draw_graph\r
+ draw_titles\r
+ draw_legend\r
+ draw_data\r
+ @graph.add_element( @foreground )\r
+ style\r
+\r
+ data = ""\r
+ @doc.write( data, 0 )\r
+\r
+ if @config[:compress]\r
+ if @@__have_zlib\r
+ inp, out = IO.pipe\r
+ gz = Zlib::GzipWriter.new( out )\r
+ gz.write data\r
+ gz.close\r
+ data = inp.read\r
+ else\r
+ data << "<!-- Ruby Zlib not available for SVGZ -->";\r
+ end\r
+ end\r
+ \r
+ return data\r
+ end\r
+\r
+\r
+ # Set the height of the graph box, this is the total height\r
+ # of the SVG box created - not the graph it self which auto\r
+ # scales to fix the space.\r
+ attr_accessor :height\r
+ # Set the width of the graph box, this is the total width\r
+ # of the SVG box created - not the graph it self which auto\r
+ # scales to fix the space.\r
+ attr_accessor :width\r
+ # Set the path to an external stylesheet, set to '' if\r
+ # you want to revert back to using the defaut internal version.\r
+ #\r
+ # To create an external stylesheet create a graph using the\r
+ # default internal version and copy the stylesheet section to\r
+ # an external file and edit from there.\r
+ attr_accessor :style_sheet\r
+ # (Bool) Show the value of each element of data on the graph\r
+ attr_accessor :show_data_values\r
+ # The point at which the Y axis starts, defaults to '0',\r
+ # if set to nil it will default to the minimum data value.\r
+ attr_accessor :min_scale_value\r
+ # Whether to show labels on the X axis or not, defaults\r
+ # to true, set to false if you want to turn them off.\r
+ attr_accessor :show_x_labels\r
+ # This puts the X labels at alternative levels so if they\r
+ # are long field names they will not overlap so easily.\r
+ # Default it false, to turn on set to true.\r
+ attr_accessor :stagger_x_labels\r
+ # This puts the Y labels at alternative levels so if they\r
+ # are long field names they will not overlap so easily.\r
+ # Default it false, to turn on set to true.\r
+ attr_accessor :stagger_y_labels\r
+ # This turns the X axis labels by 90 degrees.\r
+ # Default it false, to turn on set to true.\r
+ attr_accessor :rotate_x_labels\r
+ # This turns the Y axis labels by 90 degrees.\r
+ # Default it false, to turn on set to true.\r
+ attr_accessor :rotate_y_labels\r
+ # How many "steps" to use between displayed X axis labels,\r
+ # a step of one means display every label, a step of two results\r
+ # in every other label being displayed (label <gap> label <gap> label),\r
+ # a step of three results in every third label being displayed\r
+ # (label <gap> <gap> label <gap> <gap> label) and so on.\r
+ attr_accessor :step_x_labels\r
+ # Whether to (when taking "steps" between X axis labels) step from \r
+ # the first label (i.e. always include the first label) or step from\r
+ # the X axis origin (i.e. start with a gap if step_x_labels is greater\r
+ # than one).\r
+ attr_accessor :step_include_first_x_label\r
+ # Whether to show labels on the Y axis or not, defaults\r
+ # to true, set to false if you want to turn them off.\r
+ attr_accessor :show_y_labels\r
+ # Ensures only whole numbers are used as the scale divisions.\r
+ # Default it false, to turn on set to true. This has no effect if \r
+ # scale divisions are less than 1.\r
+ attr_accessor :scale_integers\r
+ # This defines the gap between markers on the Y axis,\r
+ # default is a 10th of the max_value, e.g. you will have\r
+ # 10 markers on the Y axis. NOTE: do not set this too\r
+ # low - you are limited to 999 markers, after that the\r
+ # graph won't generate.\r
+ attr_accessor :scale_divisions\r
+ # Whether to show the title under the X axis labels,\r
+ # default is false, set to true to show.\r
+ attr_accessor :show_x_title\r
+ # What the title under X axis should be, e.g. 'Months'.\r
+ attr_accessor :x_title\r
+ # Whether to show the title under the Y axis labels,\r
+ # default is false, set to true to show.\r
+ attr_accessor :show_y_title\r
+ # Aligns writing mode for Y axis label. \r
+ # Defaults to :bt (Bottom to Top).\r
+ # Change to :tb (Top to Bottom) to reverse.\r
+ attr_accessor :y_title_text_direction\r
+ # What the title under Y axis should be, e.g. 'Sales in thousands'.\r
+ attr_accessor :y_title\r
+ # Whether to show a title on the graph, defaults\r
+ # to false, set to true to show.\r
+ attr_accessor :show_graph_title\r
+ # What the title on the graph should be.\r
+ attr_accessor :graph_title\r
+ # Whether to show a subtitle on the graph, defaults\r
+ # to false, set to true to show.\r
+ attr_accessor :show_graph_subtitle\r
+ # What the subtitle on the graph should be.\r
+ attr_accessor :graph_subtitle\r
+ # Whether to show a key, defaults to false, set to\r
+ # true if you want to show it.\r
+ attr_accessor :key\r
+ # Where the key should be positioned, defaults to\r
+ # :right, set to :bottom if you want to move it.\r
+ attr_accessor :key_position\r
+ # Set the font size (in points) of the data point labels\r
+ attr_accessor :font_size\r
+ # Set the font size of the X axis labels\r
+ attr_accessor :x_label_font_size\r
+ # Set the font size of the X axis title\r
+ attr_accessor :x_title_font_size\r
+ # Set the font size of the Y axis labels\r
+ attr_accessor :y_label_font_size\r
+ # Set the font size of the Y axis title\r
+ attr_accessor :y_title_font_size\r
+ # Set the title font size\r
+ attr_accessor :title_font_size\r
+ # Set the subtitle font size\r
+ attr_accessor :subtitle_font_size\r
+ # Set the key font size\r
+ attr_accessor :key_font_size\r
+ # Show guidelines for the X axis\r
+ attr_accessor :show_x_guidelines\r
+ # Show guidelines for the Y axis\r
+ attr_accessor :show_y_guidelines\r
+ # Do not use CSS if set to true. Many SVG viewers do not support CSS, but\r
+ # not using CSS can result in larger SVGs as well as making it impossible to\r
+ # change colors after the chart is generated. Defaults to false.\r
+ attr_accessor :no_css\r
+ # Add popups for the data points on some graphs\r
+ attr_accessor :add_popups\r
+\r
+\r
+ protected\r
+\r
+ def sort( *arrys )\r
+ sort_multiple( arrys )\r
+ end\r
+\r
+ # Overwrite configuration options with supplied options. Used\r
+ # by subclasses.\r
+ def init_with config\r
+ config.each { |key, value|\r
+ self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym\r
+ }\r
+ end\r
+\r
+ attr_accessor :top_align, :top_font, :right_align, :right_font\r
+\r
+ KEY_BOX_SIZE = 12\r
+\r
+ # Override this (and call super) to change the margin to the left\r
+ # of the plot area. Results in @border_left being set.\r
+ def calculate_left_margin\r
+ @border_left = 7\r
+ # Check for Y labels\r
+ max_y_label_height_px = rotate_y_labels ? \r
+ y_label_font_size :\r
+ get_y_labels.max{|a,b| \r
+ a.to_s.length<=>b.to_s.length\r
+ }.to_s.length * y_label_font_size * 0.6\r
+ @border_left += max_y_label_height_px if show_y_labels\r
+ @border_left += max_y_label_height_px + 10 if stagger_y_labels\r
+ @border_left += y_title_font_size + 5 if show_y_title\r
+ end\r
+\r
+\r
+ # Calculates the width of the widest Y label. This will be the\r
+ # character height if the Y labels are rotated\r
+ def max_y_label_width_px\r
+ return font_size if rotate_y_labels\r
+ end\r
+\r
+\r
+ # Override this (and call super) to change the margin to the right\r
+ # of the plot area. Results in @border_right being set.\r
+ def calculate_right_margin\r
+ @border_right = 7\r
+ if key and key_position == :right\r
+ val = keys.max { |a,b| a.length <=> b.length }\r
+ @border_right += val.length * key_font_size * 0.6 \r
+ @border_right += KEY_BOX_SIZE\r
+ @border_right += 10 # Some padding around the box\r
+ end\r
+ end\r
+\r
+\r
+ # Override this (and call super) to change the margin to the top\r
+ # of the plot area. Results in @border_top being set.\r
+ def calculate_top_margin\r
+ @border_top = 5\r
+ @border_top += title_font_size if show_graph_title\r
+ @border_top += 5\r
+ @border_top += subtitle_font_size if show_graph_subtitle\r
+ end\r
+\r
+\r
+ # Adds pop-up point information to a graph.\r
+ def add_popup( x, y, label )\r
+ txt_width = label.length * font_size * 0.6 + 10\r
+ tx = (x+txt_width > width ? x-5 : x+5)\r
+ t = @foreground.add_element( "text", {\r
+ "x" => tx.to_s,\r
+ "y" => (y - font_size).to_s,\r
+ "visibility" => "hidden",\r
+ })\r
+ t.attributes["style"] = "fill: #000; "+\r
+ (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")\r
+ t.text = label.to_s\r
+ t.attributes["id"] = t.object_id.to_s\r
+\r
+ @foreground.add_element( "circle", {\r
+ "cx" => x.to_s,\r
+ "cy" => y.to_s,\r
+ "r" => "10",\r
+ "style" => "opacity: 0",\r
+ "onmouseover" => \r
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",\r
+ "onmouseout" => \r
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",\r
+ })\r
+\r
+ end\r
+\r
+ \r
+ # Override this (and call super) to change the margin to the bottom\r
+ # of the plot area. Results in @border_bottom being set.\r
+ def calculate_bottom_margin\r
+ @border_bottom = 7\r
+ if key and key_position == :bottom\r
+ @border_bottom += @data.size * (font_size + 5)\r
+ @border_bottom += 10\r
+ end\r
+ if show_x_labels\r
+ max_x_label_height_px = (not rotate_x_labels) ? \r
+ x_label_font_size :\r
+ get_x_labels.max{|a,b| \r
+ a.to_s.length<=>b.to_s.length\r
+ }.to_s.length * x_label_font_size * 0.6\r
+ @border_bottom += max_x_label_height_px\r
+ @border_bottom += max_x_label_height_px + 10 if stagger_x_labels\r
+ end\r
+ @border_bottom += x_title_font_size + 5 if show_x_title\r
+ end\r
+\r
+\r
+ # Draws the background, axis, and labels.\r
+ def draw_graph\r
+ @graph = @root.add_element( "g", {\r
+ "transform" => "translate( #@border_left #@border_top )"\r
+ })\r
+\r
+ # Background\r
+ @graph.add_element( "rect", {\r
+ "x" => "0",\r
+ "y" => "0",\r
+ "width" => @graph_width.to_s,\r
+ "height" => @graph_height.to_s,\r
+ "class" => "graphBackground"\r
+ })\r
+\r
+ # Axis\r
+ @graph.add_element( "path", {\r
+ "d" => "M 0 0 v#@graph_height",\r
+ "class" => "axis",\r
+ "id" => "xAxis"\r
+ })\r
+ @graph.add_element( "path", {\r
+ "d" => "M 0 #@graph_height h#@graph_width",\r
+ "class" => "axis",\r
+ "id" => "yAxis"\r
+ })\r
+\r
+ draw_x_labels\r
+ draw_y_labels\r
+ end\r
+\r
+\r
+ # Where in the X area the label is drawn\r
+ # Centered in the field, should be width/2. Start, 0.\r
+ def x_label_offset( width )\r
+ 0\r
+ end\r
+\r
+ def make_datapoint_text( x, y, value, style="" )\r
+ if show_data_values\r
+ @foreground.add_element( "text", {\r
+ "x" => x.to_s,\r
+ "y" => y.to_s,\r
+ "class" => "dataPointLabel",\r
+ "style" => "#{style} stroke: #fff; stroke-width: 2;"\r
+ }).text = value.to_s\r
+ text = @foreground.add_element( "text", {\r
+ "x" => x.to_s,\r
+ "y" => y.to_s,\r
+ "class" => "dataPointLabel"\r
+ })\r
+ text.text = value.to_s\r
+ text.attributes["style"] = style if style.length > 0\r
+ end\r
+ end\r
+\r
+\r
+ # Draws the X axis labels\r
+ def draw_x_labels\r
+ stagger = x_label_font_size + 5\r
+ if show_x_labels\r
+ label_width = field_width\r
+\r
+ count = 0\r
+ for label in get_x_labels\r
+ if step_include_first_x_label == true then\r
+ step = count % step_x_labels\r
+ else\r
+ step = (count + 1) % step_x_labels\r
+ end\r
+\r
+ if step == 0 then\r
+ text = @graph.add_element( "text" )\r
+ text.attributes["class"] = "xAxisLabels"\r
+ text.text = label.to_s\r
+\r
+ x = count * label_width + x_label_offset( label_width )\r
+ y = @graph_height + x_label_font_size + 3\r
+ t = 0 - (font_size / 2)\r
+\r
+ if stagger_x_labels and count % 2 == 1\r
+ y += stagger\r
+ @graph.add_element( "path", {\r
+ "d" => "M#{x} #@graph_height v#{stagger}",\r
+ "class" => "staggerGuideLine"\r
+ })\r
+ end\r
+\r
+ text.attributes["x"] = x.to_s\r
+ text.attributes["y"] = y.to_s\r
+ if rotate_x_labels\r
+ text.attributes["transform"] = \r
+ "rotate( 90 #{x} #{y-x_label_font_size} )"+\r
+ " translate( 0 -#{x_label_font_size/4} )"\r
+ text.attributes["style"] = "text-anchor: start"\r
+ else\r
+ text.attributes["style"] = "text-anchor: middle"\r
+ end\r
+ end\r
+\r
+ draw_x_guidelines( label_width, count ) if show_x_guidelines\r
+ count += 1\r
+ end\r
+ end\r
+ end\r
+\r
+\r
+ # Where in the Y area the label is drawn\r
+ # Centered in the field, should be width/2. Start, 0.\r
+ def y_label_offset( height )\r
+ 0\r
+ end\r
+\r
+\r
+ def field_width\r
+ (@graph_width.to_f - font_size*2*right_font) /\r
+ (get_x_labels.length - right_align)\r
+ end\r
+\r
+\r
+ def field_height\r
+ (@graph_height.to_f - font_size*2*top_font) /\r
+ (get_y_labels.length - top_align)\r
+ end\r
+\r
+\r
+ # Draws the Y axis labels\r
+ def draw_y_labels\r
+ stagger = y_label_font_size + 5\r
+ if show_y_labels\r
+ label_height = field_height\r
+\r
+ count = 0\r
+ y_offset = @graph_height + y_label_offset( label_height )\r
+ y_offset += font_size/1.2 unless rotate_y_labels\r
+ for label in get_y_labels\r
+ y = y_offset - (label_height * count)\r
+ x = rotate_y_labels ? 0 : -3\r
+\r
+ if stagger_y_labels and count % 2 == 1\r
+ x -= stagger\r
+ @graph.add_element( "path", {\r
+ "d" => "M#{x} #{y} h#{stagger}",\r
+ "class" => "staggerGuideLine"\r
+ })\r
+ end\r
+\r
+ text = @graph.add_element( "text", {\r
+ "x" => x.to_s,\r
+ "y" => y.to_s,\r
+ "class" => "yAxisLabels"\r
+ })\r
+ text.text = label.to_s\r
+ if rotate_y_labels\r
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) "+\r
+ "rotate( 90 #{x} #{y} ) "\r
+ text.attributes["style"] = "text-anchor: middle"\r
+ else\r
+ text.attributes["y"] = (y - (y_label_font_size/2)).to_s\r
+ text.attributes["style"] = "text-anchor: end"\r
+ end\r
+ draw_y_guidelines( label_height, count ) if show_y_guidelines\r
+ count += 1\r
+ end\r
+ end\r
+ end\r
+\r
+\r
+ # Draws the X axis guidelines\r
+ def draw_x_guidelines( label_height, count )\r
+ if count != 0\r
+ @graph.add_element( "path", {\r
+ "d" => "M#{label_height*count} 0 v#@graph_height",\r
+ "class" => "guideLines"\r
+ })\r
+ end\r
+ end\r
+\r
+\r
+ # Draws the Y axis guidelines\r
+ def draw_y_guidelines( label_height, count )\r
+ if count != 0\r
+ @graph.add_element( "path", {\r
+ "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",\r
+ "class" => "guideLines"\r
+ })\r
+ end\r
+ end\r
+\r
+\r
+ # Draws the graph title and subtitle\r
+ def draw_titles\r
+ if show_graph_title\r
+ @root.add_element( "text", {\r
+ "x" => (width / 2).to_s,\r
+ "y" => (title_font_size).to_s,\r
+ "class" => "mainTitle"\r
+ }).text = graph_title.to_s\r
+ end\r
+\r
+ if show_graph_subtitle\r
+ y_subtitle = show_graph_title ? \r
+ title_font_size + 10 :\r
+ subtitle_font_size\r
+ @root.add_element("text", {\r
+ "x" => (width / 2).to_s,\r
+ "y" => (y_subtitle).to_s,\r
+ "class" => "subTitle"\r
+ }).text = graph_subtitle.to_s\r
+ end\r
+\r
+ if show_x_title\r
+ y = @graph_height + @border_top + x_title_font_size\r
+ if show_x_labels\r
+ y += x_label_font_size + 5 if stagger_x_labels\r
+ y += x_label_font_size + 5\r
+ end\r
+ x = width / 2\r
+\r
+ @root.add_element("text", {\r
+ "x" => x.to_s,\r
+ "y" => y.to_s,\r
+ "class" => "xAxisTitle",\r
+ }).text = x_title.to_s\r
+ end\r
+\r
+ if show_y_title\r
+ x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)\r
+ y = height / 2\r
+\r
+ text = @root.add_element("text", {\r
+ "x" => x.to_s,\r
+ "y" => y.to_s,\r
+ "class" => "yAxisTitle",\r
+ })\r
+ text.text = y_title.to_s\r
+ if y_title_text_direction == :bt\r
+ text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"\r
+ else\r
+ text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"\r
+ end\r
+ end\r
+ end\r
+\r
+ def keys \r
+ return @data.collect{ |d| d[:title] }\r
+ end\r
+\r
+ # Draws the legend on the graph\r
+ def draw_legend\r
+ if key\r
+ group = @root.add_element( "g" )\r
+\r
+ key_count = 0\r
+ for key_name in keys\r
+ y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)\r
+ group.add_element( "rect", {\r
+ "x" => 0.to_s,\r
+ "y" => y_offset.to_s,\r
+ "width" => KEY_BOX_SIZE.to_s,\r
+ "height" => KEY_BOX_SIZE.to_s,\r
+ "class" => "key#{key_count+1}"\r
+ })\r
+ group.add_element( "text", {\r
+ "x" => (KEY_BOX_SIZE + 5).to_s,\r
+ "y" => (y_offset + KEY_BOX_SIZE).to_s,\r
+ "class" => "keyText"\r
+ }).text = key_name.to_s\r
+ key_count += 1\r
+ end\r
+\r
+ case key_position\r
+ when :right\r
+ x_offset = @graph_width + @border_left + 10\r
+ y_offset = @border_top + 20\r
+ when :bottom\r
+ x_offset = @border_left + 20\r
+ y_offset = @border_top + @graph_height + 5\r
+ if show_x_labels\r
+ max_x_label_height_px = (not rotate_x_labels) ? \r
+ x_label_font_size :\r
+ get_x_labels.max{|a,b| \r
+ a.to_s.length<=>b.to_s.length\r
+ }.to_s.length * x_label_font_size * 0.6\r
+ x_label_font_size\r
+ y_offset += max_x_label_height_px\r
+ y_offset += max_x_label_height_px + 5 if stagger_x_labels\r
+ end\r
+ y_offset += x_title_font_size + 5 if show_x_title\r
+ end\r
+ group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"\r
+ end\r
+ end\r
+\r
+\r
+ private\r
+\r
+ def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )\r
+ if lo < hi\r
+ p = partition(arrys,lo,hi)\r
+ sort_multiple(arrys, lo, p-1)\r
+ sort_multiple(arrys, p+1, hi)\r
+ end\r
+ arrys \r
+ end\r
+\r
+ def partition( arrys, lo, hi )\r
+ p = arrys[0][lo]\r
+ l = lo\r
+ z = lo+1\r
+ while z <= hi\r
+ if arrys[0][z] < p\r
+ l += 1\r
+ arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }\r
+ end\r
+ z += 1\r
+ end\r
+ arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }\r
+ l\r
+ end\r
+\r
+ def style\r
+ if no_css\r
+ styles = parse_css\r
+ @root.elements.each("//*[@class]") { |el|\r
+ cl = el.attributes["class"]\r
+ style = styles[cl]\r
+ style += el.attributes["style"] if el.attributes["style"]\r
+ el.attributes["style"] = style\r
+ }\r
+ end\r
+ end\r
+\r
+ def parse_css\r
+ css = get_style\r
+ rv = {}\r
+ while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m\r
+ names_orig = names = $1\r
+ css = $'\r
+ css =~ /([^}]+)\}/m\r
+ content = $1\r
+ css = $'\r
+\r
+ nms = []\r
+ while names =~ /^\s*,?\s*\.(\w+)/\r
+ nms << $1\r
+ names = $'\r
+ end\r
+\r
+ content = content.tr( "\n\t", " ")\r
+ for name in nms\r
+ current = rv[name]\r
+ current = current ? current+"; "+content : content\r
+ rv[name] = current.strip.squeeze(" ")\r
+ end\r
+ end\r
+ return rv\r
+ end\r
+\r
+\r
+ # Override and place code to add defs here\r
+ def add_defs defs\r
+ end\r
+\r
+\r
+ def start_svg\r
+ # Base document\r
+ @doc = Document.new\r
+ @doc << XMLDecl.new\r
+ @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +\r
+ %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )\r
+ if style_sheet && style_sheet != ''\r
+ @doc << Instruction.new( "xml-stylesheet",\r
+ %Q{href="#{style_sheet}" type="text/css"} )\r
+ end\r
+ @root = @doc.add_element( "svg", {\r
+ "width" => width.to_s,\r
+ "height" => height.to_s,\r
+ "viewBox" => "0 0 #{width} #{height}",\r
+ "xmlns" => "http://www.w3.org/2000/svg",\r
+ "xmlns:xlink" => "http://www.w3.org/1999/xlink",\r
+ "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",\r
+ "a3:scriptImplementation" => "Adobe"\r
+ })\r
+ @root << Comment.new( " "+"\\"*66 )\r
+ @root << Comment.new( " Created with SVG::Graph " )\r
+ @root << Comment.new( " SVG::Graph by Sean E. Russell " )\r
+ @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+\r
+ " Leo Lapworth & Stephan Morgan " )\r
+ @root << Comment.new( " "+"/"*66 )\r
+\r
+ defs = @root.add_element( "defs" )\r
+ add_defs defs\r
+ if not(style_sheet && style_sheet != '') and !no_css\r
+ @root << Comment.new(" include default stylesheet if none specified ")\r
+ style = defs.add_element( "style", {"type"=>"text/css"} )\r
+ style << CData.new( get_style )\r
+ end\r
+\r
+ @root << Comment.new( "SVG Background" )\r
+ @root.add_element( "rect", {\r
+ "width" => width.to_s,\r
+ "height" => height.to_s,\r
+ "x" => "0",\r
+ "y" => "0",\r
+ "class" => "svgBackground"\r
+ })\r
+ end\r
+\r
+\r
+ def calculate_graph_dimensions\r
+ calculate_left_margin\r
+ calculate_right_margin\r
+ calculate_bottom_margin\r
+ calculate_top_margin\r
+ @graph_width = width - @border_left - @border_right\r
+ @graph_height = height - @border_top - @border_bottom\r
+ end\r
+\r
+ def get_style\r
+ return <<EOL\r
+/* Copy from here for external style sheet */\r
+.svgBackground{\r
+ fill:#ffffff;\r
+}\r
+.graphBackground{\r
+ fill:#f0f0f0;\r
+}\r
+\r
+/* graphs titles */\r
+.mainTitle{\r
+ text-anchor: middle;\r
+ fill: #000000;\r
+ font-size: #{title_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+.subTitle{\r
+ text-anchor: middle;\r
+ fill: #999999;\r
+ font-size: #{subtitle_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.axis{\r
+ stroke: #000000;\r
+ stroke-width: 1px;\r
+}\r
+\r
+.guideLines{\r
+ stroke: #666666;\r
+ stroke-width: 1px;\r
+ stroke-dasharray: 5 5;\r
+}\r
+\r
+.xAxisLabels{\r
+ text-anchor: middle;\r
+ fill: #000000;\r
+ font-size: #{x_label_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.yAxisLabels{\r
+ text-anchor: end;\r
+ fill: #000000;\r
+ font-size: #{y_label_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.xAxisTitle{\r
+ text-anchor: middle;\r
+ fill: #ff0000;\r
+ font-size: #{x_title_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.yAxisTitle{\r
+ fill: #ff0000;\r
+ text-anchor: middle;\r
+ font-size: #{y_title_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.dataPointLabel{\r
+ fill: #000000;\r
+ text-anchor:middle;\r
+ font-size: 10px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+.staggerGuideLine{\r
+ fill: none;\r
+ stroke: #000000;\r
+ stroke-width: 0.5px; \r
+}\r
+\r
+#{get_css}\r
+\r
+.keyText{\r
+ fill: #000000;\r
+ text-anchor:start;\r
+ font-size: #{key_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+/* End copy for external style sheet */\r
+EOL\r
+ end\r
+\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'SVG/Graph/Graph'
+
+module SVG
+ module Graph
+ # === Create presentation quality SVG line graphs easily
+ #
+ # = Synopsis
+ #
+ # require 'SVG/Graph/Line'
+ #
+ # fields = %w(Jan Feb Mar);
+ # data_sales_02 = [12, 45, 21]
+ # data_sales_03 = [15, 30, 40]
+ #
+ # graph = SVG::Graph::Line.new({
+ # :height => 500,
+ # :width => 300,
+ # :fields => fields,
+ # })
+ #
+ # graph.add_data({
+ # :data => data_sales_02,
+ # :title => 'Sales 2002',
+ # })
+ #
+ # graph.add_data({
+ # :data => data_sales_03,
+ # :title => 'Sales 2003',
+ # })
+ #
+ # print "Content-type: image/svg+xml\r\n\r\n";
+ # print graph.burn();
+ #
+ # = Description
+ #
+ # This object aims to allow you to easily create high quality
+ # SVG line graphs. You can either use the default style sheet
+ # or supply your own. Either way there are many options which can
+ # be configured to give you control over how the graph is
+ # generated - with or without a key, data elements at each point,
+ # title, subtitle etc.
+ #
+ # = Examples
+ #
+ # http://www.germane-software/repositories/public/SVG/test/single.rb
+ #
+ # = Notes
+ #
+ # The default stylesheet handles upto 10 data sets, if you
+ # use more you must create your own stylesheet and add the
+ # additional settings for the extra data sets. You will know
+ # if you go over 10 data sets as they will have no style and
+ # be in black.
+ #
+ # = See also
+ #
+ # * SVG::Graph::Graph
+ # * SVG::Graph::BarHorizontal
+ # * SVG::Graph::Bar
+ # * SVG::Graph::Pie
+ # * SVG::Graph::Plot
+ # * SVG::Graph::TimeSeries
+ #
+ # == Author
+ #
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
+ #
+ # Copyright 2004 Sean E. Russell
+ # This software is available under the Ruby license[LICENSE.txt]
+ #
+ class Line < SVG::Graph::Graph
+ # Show a small circle on the graph where the line
+ # goes from one point to the next.
+ attr_accessor :show_data_points
+ # Accumulates each data set. (i.e. Each point increased by sum of
+ # all previous series at same point). Default is 0, set to '1' to show.
+ attr_accessor :stacked
+ # Fill in the area under the plot if true
+ attr_accessor :area_fill
+
+ # The constructor takes a hash reference, fields (the names for each
+ # field on the X axis) MUST be set, all other values are defaulted to
+ # those shown above - with the exception of style_sheet which defaults
+ # to using the internal style sheet.
+ def initialize config
+ raise "fields was not supplied or is empty" unless config[:fields] &&
+ config[:fields].kind_of?(Array) &&
+ config[:fields].length > 0
+ super
+ end
+
+ # In addition to the defaults set in Graph::initialize, sets
+ # [show_data_points] true
+ # [show_data_values] true
+ # [stacked] false
+ # [area_fill] false
+ def set_defaults
+ init_with(
+ :show_data_points => true,
+ :show_data_values => true,
+ :stacked => false,
+ :area_fill => false
+ )
+
+ self.top_align = self.top_font = self.right_align = self.right_font = 1
+ end
+
+ protected
+
+ def max_value
+ max = 0
+
+ if (stacked == true) then
+ sums = Array.new(@config[:fields].length).fill(0)
+
+ @data.each do |data|
+ sums.each_index do |i|
+ sums[i] += data[:data][i].to_f
+ end
+ end
+
+ max = sums.max
+ else
+ max = @data.collect{|x| x[:data].max}.max
+ end
+
+ return max
+ end
+
+ def min_value
+ min = 0
+
+ if (min_scale_value.nil? == false) then
+ min = min_scale_value
+ elsif (stacked == true) then
+ min = @data[-1][:data].min
+ else
+ min = @data.collect{|x| x[:data].min}.min
+ end
+
+ return min
+ end
+
+ def get_x_labels
+ @config[:fields]
+ end
+
+ def calculate_left_margin
+ super
+ label_left = @config[:fields][0].length / 2 * font_size * 0.6
+ @border_left = label_left if label_left > @border_left
+ end
+
+ def get_y_labels
+ maxvalue = max_value
+ minvalue = min_value
+ range = maxvalue - minvalue
+ top_pad = range == 0 ? 10 : range / 20.0
+ scale_range = (maxvalue + top_pad) - minvalue
+
+ scale_division = scale_divisions || (scale_range / 10.0)
+
+ if scale_integers
+ scale_division = scale_division < 1 ? 1 : scale_division.round
+ end
+
+ rv = []
+ maxvalue = maxvalue%scale_division == 0 ?
+ maxvalue : maxvalue + scale_division
+ minvalue.step( maxvalue, scale_division ) {|v| rv << v}
+ return rv
+ end
+
+ def calc_coords(field, value, width = field_width, height = field_height)
+ coords = {:x => 0, :y => 0}
+ coords[:x] = width * field
+ coords[:y] = @graph_height - value * height
+
+ return coords
+ end
+
+ def draw_data
+ minvalue = min_value
+ fieldheight = (@graph_height.to_f - font_size*2*top_font) /
+ (get_y_labels.max - get_y_labels.min)
+ fieldwidth = field_width
+ line = @data.length
+
+ prev_sum = Array.new(@config[:fields].length).fill(0)
+ cum_sum = Array.new(@config[:fields].length).fill(-minvalue)
+
+ for data in @data.reverse
+ lpath = ""
+ apath = ""
+
+ if not stacked then cum_sum.fill(-minvalue) end
+
+ data[:data].each_index do |i|
+ cum_sum[i] += data[:data][i]
+
+ c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
+
+ lpath << "#{c[:x]} #{c[:y]} "
+ end
+
+ if area_fill
+ if stacked then
+ (prev_sum.length - 1).downto 0 do |i|
+ c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
+
+ apath << "#{c[:x]} #{c[:y]} "
+ end
+
+ c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
+ else
+ apath = "V#@graph_height"
+ c = calc_coords(0, 0, fieldwidth, fieldheight)
+ end
+
+ @graph.add_element("path", {
+ "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
+ "class" => "fill#{line}"
+ })
+ end
+
+ @graph.add_element("path", {
+ "d" => "M0 #@graph_height L" + lpath,
+ "class" => "line#{line}"
+ })
+
+ if show_data_points || show_data_values
+ cum_sum.each_index do |i|
+ if show_data_points
+ @graph.add_element( "circle", {
+ "cx" => (fieldwidth * i).to_s,
+ "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
+ "r" => "2.5",
+ "class" => "dataPoint#{line}"
+ })
+ end
+ make_datapoint_text(
+ fieldwidth * i,
+ @graph_height - cum_sum[i] * fieldheight - 6,
+ cum_sum[i] + minvalue
+ )
+ end
+ end
+
+ prev_sum = cum_sum.dup
+ line -= 1
+ end
+ end
+
+
+ def get_css
+ return <<EOL
+/* default line styles */
+.line1{
+ fill: none;
+ stroke: #ff0000;
+ stroke-width: 1px;
+}
+.line2{
+ fill: none;
+ stroke: #0000ff;
+ stroke-width: 1px;
+}
+.line3{
+ fill: none;
+ stroke: #00ff00;
+ stroke-width: 1px;
+}
+.line4{
+ fill: none;
+ stroke: #ffcc00;
+ stroke-width: 1px;
+}
+.line5{
+ fill: none;
+ stroke: #00ccff;
+ stroke-width: 1px;
+}
+.line6{
+ fill: none;
+ stroke: #ff00ff;
+ stroke-width: 1px;
+}
+.line7{
+ fill: none;
+ stroke: #00ffff;
+ stroke-width: 1px;
+}
+.line8{
+ fill: none;
+ stroke: #ffff00;
+ stroke-width: 1px;
+}
+.line9{
+ fill: none;
+ stroke: #ccc6666;
+ stroke-width: 1px;
+}
+.line10{
+ fill: none;
+ stroke: #663399;
+ stroke-width: 1px;
+}
+.line11{
+ fill: none;
+ stroke: #339900;
+ stroke-width: 1px;
+}
+.line12{
+ fill: none;
+ stroke: #9966FF;
+ stroke-width: 1px;
+}
+/* default fill styles */
+.fill1{
+ fill: #cc0000;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill2{
+ fill: #0000cc;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill3{
+ fill: #00cc00;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill4{
+ fill: #ffcc00;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill5{
+ fill: #00ccff;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill6{
+ fill: #ff00ff;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill7{
+ fill: #00ffff;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill8{
+ fill: #ffff00;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill9{
+ fill: #cc6666;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill10{
+ fill: #663399;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill11{
+ fill: #339900;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+.fill12{
+ fill: #9966FF;
+ fill-opacity: 0.2;
+ stroke: none;
+}
+/* default line styles */
+.key1,.dataPoint1{
+ fill: #ff0000;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key2,.dataPoint2{
+ fill: #0000ff;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key3,.dataPoint3{
+ fill: #00ff00;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key4,.dataPoint4{
+ fill: #ffcc00;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key5,.dataPoint5{
+ fill: #00ccff;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key6,.dataPoint6{
+ fill: #ff00ff;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key7,.dataPoint7{
+ fill: #00ffff;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key8,.dataPoint8{
+ fill: #ffff00;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key9,.dataPoint9{
+ fill: #cc6666;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key10,.dataPoint10{
+ fill: #663399;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key11,.dataPoint11{
+ fill: #339900;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key12,.dataPoint12{
+ fill: #9966FF;
+ stroke: none;
+ stroke-width: 1px;
+}
+EOL
+ end
+ end
+ end
+end
--- /dev/null
+require 'SVG/Graph/Graph'\r
+\r
+module SVG\r
+ module Graph\r
+ # === Create presentation quality SVG pie graphs easily\r
+ # \r
+ # == Synopsis\r
+ # \r
+ # require 'SVG/Graph/Pie'\r
+ # \r
+ # fields = %w(Jan Feb Mar)\r
+ # data_sales_02 = [12, 45, 21]\r
+ # \r
+ # graph = SVG::Graph::Pie.new({\r
+ # :height => 500,\r
+ # :width => 300,\r
+ # :fields => fields,\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => data_sales_02,\r
+ # :title => 'Sales 2002',\r
+ # })\r
+ # \r
+ # print "Content-type: image/svg+xml\r\n\r\n"\r
+ # print graph.burn();\r
+ # \r
+ # == Description\r
+ # \r
+ # This object aims to allow you to easily create high quality\r
+ # SVG pie graphs. You can either use the default style sheet\r
+ # or supply your own. Either way there are many options which can\r
+ # be configured to give you control over how the graph is\r
+ # generated - with or without a key, display percent on pie chart,\r
+ # title, subtitle etc.\r
+ #\r
+ # = Examples\r
+ # \r
+ # http://www.germane-software/repositories/public/SVG/test/single.rb\r
+ # \r
+ # == See also\r
+ #\r
+ # * SVG::Graph::Graph\r
+ # * SVG::Graph::BarHorizontal\r
+ # * SVG::Graph::Bar\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Plot\r
+ # * SVG::Graph::TimeSeries\r
+ #\r
+ # == Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class Pie < Graph\r
+ # Defaults are those set by Graph::initialize, and\r
+ # [show_shadow] true\r
+ # [shadow_offset] 10\r
+ # [show_data_labels] false\r
+ # [show_actual_values] false\r
+ # [show_percent] true\r
+ # [show_key_data_labels] true\r
+ # [show_key_actual_values] true\r
+ # [show_key_percent] false\r
+ # [expanded] false\r
+ # [expand_greatest] false\r
+ # [expand_gap] 10\r
+ # [show_x_labels] false\r
+ # [show_y_labels] false\r
+ # [datapoint_font_size] 12\r
+ def set_defaults\r
+ init_with(\r
+ :show_shadow => true,\r
+ :shadow_offset => 10, \r
+ \r
+ :show_data_labels => false,\r
+ :show_actual_values => false,\r
+ :show_percent => true,\r
+\r
+ :show_key_data_labels => true,\r
+ :show_key_actual_values => true,\r
+ :show_key_percent => false,\r
+ \r
+ :expanded => false,\r
+ :expand_greatest => false,\r
+ :expand_gap => 10,\r
+ \r
+ :show_x_labels => false,\r
+ :show_y_labels => false,\r
+ :datapoint_font_size => 12\r
+ )\r
+ @data = []\r
+ end\r
+\r
+ # Adds a data set to the graph.\r
+ #\r
+ # graph.add_data( { :data => [1,2,3,4] } )\r
+ #\r
+ # Note that the :title is not necessary. If multiple\r
+ # data sets are added to the graph, the pie chart will\r
+ # display the +sums+ of the data. EG:\r
+ #\r
+ # graph.add_data( { :data => [1,2,3,4] } )\r
+ # graph.add_data( { :data => [2,3,5,9] } )\r
+ #\r
+ # is the same as:\r
+ #\r
+ # graph.add_data( { :data => [3,5,8,13] } )\r
+ def add_data arg\r
+ arg[:data].each_index {|idx|\r
+ @data[idx] = 0 unless @data[idx]\r
+ @data[idx] += arg[:data][idx]\r
+ }\r
+ end\r
+\r
+ # If true, displays a drop shadow for the chart\r
+ attr_accessor :show_shadow \r
+ # Sets the offset of the shadow from the pie chart\r
+ attr_accessor :shadow_offset\r
+ # If true, display the data labels on the chart\r
+ attr_accessor :show_data_labels \r
+ # If true, display the actual field values in the data labels\r
+ attr_accessor :show_actual_values \r
+ # If true, display the percentage value of each pie wedge in the data\r
+ # labels\r
+ attr_accessor :show_percent\r
+ # If true, display the labels in the key\r
+ attr_accessor :show_key_data_labels \r
+ # If true, display the actual value of the field in the key\r
+ attr_accessor :show_key_actual_values \r
+ # If true, display the percentage value of the wedges in the key\r
+ attr_accessor :show_key_percent\r
+ # If true, "explode" the pie (put space between the wedges)\r
+ attr_accessor :expanded \r
+ # If true, expand the largest pie wedge\r
+ attr_accessor :expand_greatest \r
+ # The amount of space between expanded wedges\r
+ attr_accessor :expand_gap \r
+ # The font size of the data point labels\r
+ attr_accessor :datapoint_font_size\r
+\r
+\r
+ protected\r
+\r
+ def add_defs defs\r
+ gradient = defs.add_element( "filter", {\r
+ "id"=>"dropshadow",\r
+ "width" => "1.2",\r
+ "height" => "1.2",\r
+ } )\r
+ gradient.add_element( "feGaussianBlur", {\r
+ "stdDeviation" => "4",\r
+ "result" => "blur"\r
+ })\r
+ end\r
+\r
+ # We don't need the graph\r
+ def draw_graph\r
+ end\r
+\r
+ def get_y_labels\r
+ [""]\r
+ end\r
+\r
+ def get_x_labels\r
+ [""]\r
+ end\r
+\r
+ def keys\r
+ total = 0\r
+ max_value = 0\r
+ @data.each {|x| total += x }\r
+ percent_scale = 100.0 / total\r
+ count = -1\r
+ a = @config[:fields].collect{ |x|\r
+ count += 1\r
+ v = @data[count]\r
+ perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""\r
+ x + " [" + v.to_s + "]" + perc\r
+ }\r
+ end\r
+\r
+ RADIANS = Math::PI/180\r
+\r
+ def draw_data\r
+ @graph = @root.add_element( "g" )\r
+ background = @graph.add_element("g")\r
+ midground = @graph.add_element("g")\r
+\r
+ diameter = @graph_height > @graph_width ? @graph_width : @graph_height\r
+ diameter -= expand_gap if expanded or expand_greatest\r
+ diameter -= datapoint_font_size if show_data_labels\r
+ diameter -= 10 if show_shadow\r
+ radius = diameter / 2.0\r
+\r
+ xoff = (width - diameter) / 2\r
+ yoff = (height - @border_bottom - diameter)\r
+ yoff -= 10 if show_shadow\r
+ @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"\r
+\r
+ wedge_text_pad = 5\r
+ wedge_text_pad = 20 if show_percent and show_data_labels\r
+\r
+ total = 0\r
+ max_value = 0\r
+ @data.each {|x| \r
+ max_value = max_value < x ? x : max_value\r
+ total += x \r
+ }\r
+ percent_scale = 100.0 / total\r
+\r
+ prev_percent = 0\r
+ rad_mult = 3.6 * RADIANS\r
+ @config[:fields].each_index { |count|\r
+ value = @data[count]\r
+ percent = percent_scale * value\r
+\r
+ radians = prev_percent * rad_mult\r
+ x_start = radius+(Math.sin(radians) * radius)\r
+ y_start = radius-(Math.cos(radians) * radius)\r
+ radians = (prev_percent+percent) * rad_mult\r
+ x_end = radius+(Math.sin(radians) * radius)\r
+ x_end -= 0.00001 if @data.length == 1\r
+ y_end = radius-(Math.cos(radians) * radius)\r
+ path = "M#{radius},#{radius} L#{x_start},#{y_start} "+\r
+ "A#{radius},#{radius} "+\r
+ "0, #{percent >= 50 ? '1' : '0'},1, "+\r
+ "#{x_end} #{y_end} Z"\r
+\r
+\r
+ wedge = @foreground.add_element( "path", {\r
+ "d" => path,\r
+ "class" => "fill#{count+1}"\r
+ })\r
+\r
+ translate = nil\r
+ tx = 0\r
+ ty = 0\r
+ half_percent = prev_percent + percent / 2\r
+ radians = half_percent * rad_mult\r
+\r
+ if show_shadow\r
+ shadow = background.add_element( "path", {\r
+ "d" => path,\r
+ "filter" => "url(#dropshadow)",\r
+ "style" => "fill: #ccc; stroke: none;"\r
+ })\r
+ clear = midground.add_element( "path", {\r
+ "d" => path,\r
+ "style" => "fill: #fff; stroke: none;"\r
+ })\r
+ end\r
+\r
+ if expanded or (expand_greatest && value == max_value)\r
+ tx = (Math.sin(radians) * expand_gap)\r
+ ty = -(Math.cos(radians) * expand_gap)\r
+ translate = "translate( #{tx} #{ty} )"\r
+ wedge.attributes["transform"] = translate\r
+ clear.attributes["transform"] = translate if clear\r
+ end\r
+\r
+ if show_shadow\r
+ shadow.attributes["transform"] = \r
+ "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"\r
+ end\r
+ \r
+ if show_data_labels and value != 0\r
+ label = ""\r
+ label += @config[:fields][count] if show_key_data_labels\r
+ label += " ["+value.to_s+"]" if show_actual_values\r
+ label += " "+percent.round.to_s+"%" if show_percent\r
+\r
+ msr = Math.sin(radians)\r
+ mcr = Math.cos(radians)\r
+ tx = radius + (msr * radius)\r
+ ty = radius -(mcr * radius)\r
+\r
+ if expanded or (expand_greatest && value == max_value)\r
+ tx += (msr * expand_gap)\r
+ ty -= (mcr * expand_gap)\r
+ end\r
+ @foreground.add_element( "text", {\r
+ "x" => tx.to_s,\r
+ "y" => ty.to_s,\r
+ "class" => "dataPointLabel",\r
+ "style" => "stroke: #fff; stroke-width: 2;"\r
+ }).text = label.to_s\r
+ @foreground.add_element( "text", {\r
+ "x" => tx.to_s,\r
+ "y" => ty.to_s,\r
+ "class" => "dataPointLabel",\r
+ }).text = label.to_s\r
+ end\r
+\r
+ prev_percent += percent\r
+ }\r
+ end\r
+ \r
+\r
+ def round val, to\r
+ up = 10**to.to_f\r
+ (val * up).to_i / up\r
+ end\r
+\r
+\r
+ def get_css\r
+ return <<EOL\r
+.dataPointLabel{\r
+ fill: #000000;\r
+ text-anchor:middle;\r
+ font-size: #{datapoint_font_size}px;\r
+ font-family: "Arial", sans-serif;\r
+ font-weight: normal;\r
+}\r
+\r
+/* key - MUST match fill styles */\r
+.key1,.fill1{\r
+ fill: #ff0000;\r
+ fill-opacity: 0.7;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key2,.fill2{\r
+ fill: #0000ff;\r
+ fill-opacity: 0.7;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key3,.fill3{\r
+ fill-opacity: 0.7;\r
+ fill: #00ff00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key4,.fill4{\r
+ fill-opacity: 0.7;\r
+ fill: #ffcc00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key5,.fill5{\r
+ fill-opacity: 0.7;\r
+ fill: #00ccff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key6,.fill6{\r
+ fill-opacity: 0.7;\r
+ fill: #ff00ff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key7,.fill7{\r
+ fill-opacity: 0.7;\r
+ fill: #00ff99;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key8,.fill8{\r
+ fill-opacity: 0.7;\r
+ fill: #ffff00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key9,.fill9{\r
+ fill-opacity: 0.7;\r
+ fill: #cc6666;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key10,.fill10{\r
+ fill-opacity: 0.7;\r
+ fill: #663399;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key11,.fill11{\r
+ fill-opacity: 0.7;\r
+ fill: #339900;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key12,.fill12{\r
+ fill-opacity: 0.7;\r
+ fill: #9966FF;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+EOL\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'SVG/Graph/Graph'\r
+\r
+module SVG\r
+ module Graph\r
+ # === For creating SVG plots of scalar data\r
+ # \r
+ # = Synopsis\r
+ # \r
+ # require 'SVG/Graph/Plot'\r
+ # \r
+ # # Data sets are x,y pairs\r
+ # # Note that multiple data sets can differ in length, and that the\r
+ # # data in the datasets needn't be in order; they will be ordered\r
+ # # by the plot along the X-axis.\r
+ # projection = [\r
+ # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,\r
+ # 7, 9 \r
+ # ]\r
+ # actual = [\r
+ # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, \r
+ # 15, 6, 4, 17, 2, 12\r
+ # ]\r
+ # \r
+ # graph = SVG::Graph::Plot.new({\r
+ # :height => 500,\r
+ # :width => 300,\r
+ # :key => true,\r
+ # :scale_x_integers => true,\r
+ # :scale_y_integerrs => true,\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => projection\r
+ # :title => 'Projected',\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => actual,\r
+ # :title => 'Actual',\r
+ # })\r
+ # \r
+ # print graph.burn()\r
+ # \r
+ # = Description\r
+ # \r
+ # Produces a graph of scalar data.\r
+ # \r
+ # This object aims to allow you to easily create high quality\r
+ # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the\r
+ # default style sheet or supply your own. Either way there are many options\r
+ # which can be configured to give you control over how the graph is\r
+ # generated - with or without a key, data elements at each point, title,\r
+ # subtitle etc.\r
+ #\r
+ # = Examples\r
+ # \r
+ # http://www.germane-software/repositories/public/SVG/test/plot.rb\r
+ # \r
+ # = Notes\r
+ # \r
+ # The default stylesheet handles upto 10 data sets, if you\r
+ # use more you must create your own stylesheet and add the\r
+ # additional settings for the extra data sets. You will know\r
+ # if you go over 10 data sets as they will have no style and\r
+ # be in black.\r
+ #\r
+ # Unlike the other types of charts, data sets must contain x,y pairs:\r
+ #\r
+ # [ 1, 2 ] # A data set with 1 point: (1,2)\r
+ # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) \r
+ # \r
+ # = See also\r
+ # \r
+ # * SVG::Graph::Graph\r
+ # * SVG::Graph::BarHorizontal\r
+ # * SVG::Graph::Bar\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Pie\r
+ # * SVG::Graph::TimeSeries\r
+ #\r
+ # == Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class Plot < Graph\r
+\r
+ # In addition to the defaults set by Graph::initialize, sets\r
+ # [show_data_values] true\r
+ # [show_data_points] true\r
+ # [area_fill] false\r
+ # [stacked] false\r
+ def set_defaults\r
+ init_with(\r
+ :show_data_values => true,\r
+ :show_data_points => true,\r
+ :area_fill => false,\r
+ :stacked => false\r
+ )\r
+ self.top_align = self.right_align = self.top_font = self.right_font = 1\r
+ end\r
+\r
+ # Determines the scaling for the X axis divisions.\r
+ #\r
+ # graph.scale_x_divisions = 2\r
+ #\r
+ # would cause the graph to attempt to generate labels stepped by 2; EG:\r
+ # 0,2,4,6,8...\r
+ attr_accessor :scale_x_divisions\r
+ # Determines the scaling for the Y axis divisions.\r
+ #\r
+ # graph.scale_y_divisions = 0.5\r
+ #\r
+ # would cause the graph to attempt to generate labels stepped by 0.5; EG:\r
+ # 0, 0.5, 1, 1.5, 2, ...\r
+ attr_accessor :scale_y_divisions \r
+ # Make the X axis labels integers\r
+ attr_accessor :scale_x_integers \r
+ # Make the Y axis labels integers\r
+ attr_accessor :scale_y_integers \r
+ # Fill the area under the line\r
+ attr_accessor :area_fill \r
+ # Show a small circle on the graph where the line\r
+ # goes from one point to the next.\r
+ attr_accessor :show_data_points\r
+ # Set the minimum value of the X axis\r
+ attr_accessor :min_x_value \r
+ # Set the minimum value of the Y axis\r
+ attr_accessor :min_y_value\r
+\r
+\r
+ # Adds data to the plot. The data must be in X,Y pairs; EG\r
+ # [ 1, 2 ] # A data set with 1 point: (1,2)\r
+ # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) \r
+ def add_data data\r
+ @data = [] unless @data\r
+\r
+ raise "No data provided by #{conf.inspect}" unless data[:data] and\r
+ data[:data].kind_of? Array\r
+ raise "Data supplied must be x,y pairs! "+\r
+ "The data provided contained an odd set of "+\r
+ "data points" unless data[:data].length % 2 == 0\r
+ return if data[:data].length == 0\r
+\r
+ x = []\r
+ y = []\r
+ data[:data].each_index {|i|\r
+ (i%2 == 0 ? x : y) << data[:data][i]\r
+ }\r
+ sort( x, y )\r
+ data[:data] = [x,y]\r
+ @data << data\r
+ end\r
+\r
+\r
+ protected\r
+\r
+ def keys\r
+ @data.collect{ |x| x[:title] }\r
+ end\r
+\r
+ def calculate_left_margin\r
+ super\r
+ label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6\r
+ @border_left = label_left if label_left > @border_left\r
+ end\r
+\r
+ def calculate_right_margin\r
+ super\r
+ label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6\r
+ @border_right = label_right if label_right > @border_right\r
+ end\r
+\r
+\r
+ X = 0\r
+ Y = 1\r
+ def x_range\r
+ max_value = @data.collect{|x| x[:data][X][-1] }.max\r
+ min_value = @data.collect{|x| x[:data][X][0] }.min\r
+ min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value\r
+\r
+ range = max_value - min_value\r
+ right_pad = range == 0 ? 10 : range / 20.0\r
+ scale_range = (max_value + right_pad) - min_value\r
+\r
+ scale_division = scale_x_divisions || (scale_range / 10.0)\r
+\r
+ if scale_x_integers\r
+ scale_division = scale_division < 1 ? 1 : scale_division.round\r
+ end\r
+\r
+ [min_value, max_value, scale_division]\r
+ end\r
+\r
+ def get_x_values\r
+ min_value, max_value, scale_division = x_range\r
+ rv = []\r
+ min_value.step( max_value, scale_division ) {|v| rv << v}\r
+ return rv\r
+ end\r
+ alias :get_x_labels :get_x_values\r
+\r
+ def field_width\r
+ values = get_x_values\r
+ max = @data.collect{|x| x[:data][X][-1]}.max\r
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])\r
+ (@graph_width.to_f - font_size*2*right_font) /\r
+ (values.length + dx - right_align)\r
+ end\r
+\r
+\r
+ def y_range\r
+ max_value = @data.collect{|x| x[:data][Y].max }.max\r
+ min_value = @data.collect{|x| x[:data][Y].min }.min\r
+ min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value\r
+\r
+ range = max_value - min_value\r
+ top_pad = range == 0 ? 10 : range / 20.0\r
+ scale_range = (max_value + top_pad) - min_value\r
+\r
+ scale_division = scale_y_divisions || (scale_range / 10.0)\r
+\r
+ if scale_y_integers\r
+ scale_division = scale_division < 1 ? 1 : scale_division.round\r
+ end\r
+\r
+ return [min_value, max_value, scale_division]\r
+ end\r
+\r
+ def get_y_values\r
+ min_value, max_value, scale_division = y_range\r
+ rv = []\r
+ min_value.step( max_value, scale_division ) {|v| rv << v}\r
+ return rv\r
+ end\r
+ alias :get_y_labels :get_y_values\r
+\r
+ def field_height\r
+ values = get_y_values\r
+ max = @data.collect{|x| x[:data][Y].max }.max\r
+ if values.length == 1\r
+ dx = values[-1]\r
+ else\r
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])\r
+ end\r
+ (@graph_height.to_f - font_size*2*top_font) /\r
+ (values.length + dx - top_align)\r
+ end\r
+\r
+ def draw_data\r
+ line = 1\r
+\r
+ x_min, x_max, x_div = x_range\r
+ y_min, y_max, y_div = y_range\r
+ x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)\r
+ y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)\r
+\r
+ for data in @data\r
+ x_points = data[:data][X]\r
+ y_points = data[:data][Y]\r
+\r
+ lpath = "L"\r
+ x_start = 0\r
+ y_start = 0\r
+ x_points.each_index { |idx|\r
+ x = (x_points[idx] - x_min) * x_step\r
+ y = @graph_height - (y_points[idx] - y_min) * y_step\r
+ x_start, y_start = x,y if idx == 0\r
+ lpath << "#{x} #{y} "\r
+ }\r
+\r
+ if area_fill\r
+ @graph.add_element( "path", {\r
+ "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",\r
+ "class" => "fill#{line}"\r
+ })\r
+ end\r
+\r
+ @graph.add_element( "path", {\r
+ "d" => "M#{x_start} #{y_start} #{lpath}",\r
+ "class" => "line#{line}"\r
+ })\r
+\r
+ if show_data_points || show_data_values\r
+ x_points.each_index { |idx|\r
+ x = (x_points[idx] - x_min) * x_step\r
+ y = @graph_height - (y_points[idx] - y_min) * y_step\r
+ if show_data_points\r
+ @graph.add_element( "circle", {\r
+ "cx" => x.to_s,\r
+ "cy" => y.to_s,\r
+ "r" => "2.5",\r
+ "class" => "dataPoint#{line}"\r
+ })\r
+ add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups\r
+ end\r
+ make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values\r
+ }\r
+ end\r
+ line += 1\r
+ end\r
+ end\r
+\r
+ def format x, y\r
+ "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"\r
+ end\r
+ \r
+ def get_css\r
+ return <<EOL\r
+/* default line styles */\r
+.line1{\r
+ fill: none;\r
+ stroke: #ff0000;\r
+ stroke-width: 1px; \r
+}\r
+.line2{\r
+ fill: none;\r
+ stroke: #0000ff;\r
+ stroke-width: 1px; \r
+}\r
+.line3{\r
+ fill: none;\r
+ stroke: #00ff00;\r
+ stroke-width: 1px; \r
+}\r
+.line4{\r
+ fill: none;\r
+ stroke: #ffcc00;\r
+ stroke-width: 1px; \r
+}\r
+.line5{\r
+ fill: none;\r
+ stroke: #00ccff;\r
+ stroke-width: 1px; \r
+}\r
+.line6{\r
+ fill: none;\r
+ stroke: #ff00ff;\r
+ stroke-width: 1px; \r
+}\r
+.line7{\r
+ fill: none;\r
+ stroke: #00ffff;\r
+ stroke-width: 1px; \r
+}\r
+.line8{\r
+ fill: none;\r
+ stroke: #ffff00;\r
+ stroke-width: 1px; \r
+}\r
+.line9{\r
+ fill: none;\r
+ stroke: #ccc6666;\r
+ stroke-width: 1px; \r
+}\r
+.line10{\r
+ fill: none;\r
+ stroke: #663399;\r
+ stroke-width: 1px; \r
+}\r
+.line11{\r
+ fill: none;\r
+ stroke: #339900;\r
+ stroke-width: 1px; \r
+}\r
+.line12{\r
+ fill: none;\r
+ stroke: #9966FF;\r
+ stroke-width: 1px; \r
+}\r
+/* default fill styles */\r
+.fill1{\r
+ fill: #cc0000;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill2{\r
+ fill: #0000cc;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill3{\r
+ fill: #00cc00;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill4{\r
+ fill: #ffcc00;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill5{\r
+ fill: #00ccff;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill6{\r
+ fill: #ff00ff;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill7{\r
+ fill: #00ffff;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill8{\r
+ fill: #ffff00;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill9{\r
+ fill: #cc6666;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill10{\r
+ fill: #663399;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill11{\r
+ fill: #339900;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+.fill12{\r
+ fill: #9966FF;\r
+ fill-opacity: 0.2;\r
+ stroke: none;\r
+}\r
+/* default line styles */\r
+.key1,.dataPoint1{\r
+ fill: #ff0000;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key2,.dataPoint2{\r
+ fill: #0000ff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key3,.dataPoint3{\r
+ fill: #00ff00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key4,.dataPoint4{\r
+ fill: #ffcc00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key5,.dataPoint5{\r
+ fill: #00ccff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key6,.dataPoint6{\r
+ fill: #ff00ff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key7,.dataPoint7{\r
+ fill: #00ffff;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key8,.dataPoint8{\r
+ fill: #ffff00;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key9,.dataPoint9{\r
+ fill: #cc6666;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key10,.dataPoint10{\r
+ fill: #663399;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key11,.dataPoint11{\r
+ fill: #339900;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+.key12,.dataPoint12{\r
+ fill: #9966FF;\r
+ stroke: none;\r
+ stroke-width: 1px; \r
+}\r
+EOL\r
+ end\r
+\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'SVG/Graph/Plot'
+require 'parsedate'
+
+module SVG
+ module Graph
+ # === For creating SVG plots of scalar temporal data
+ #
+ # = Synopsis
+ #
+ # require 'SVG/Graph/Schedule'
+ #
+ # # Data sets are label, start, end tripples.
+ # data1 = [
+ # "Housesitting", "6/17/04", "6/19/04",
+ # "Summer Session", "6/15/04", "8/15/04",
+ # ]
+ #
+ # graph = SVG::Graph::Schedule.new( {
+ # :width => 640,
+ # :height => 480,
+ # :graph_title => title,
+ # :show_graph_title => true,
+ # :no_css => true,
+ # :scale_x_integers => true,
+ # :scale_y_integers => true,
+ # :min_x_value => 0,
+ # :min_y_value => 0,
+ # :show_data_labels => true,
+ # :show_x_guidelines => true,
+ # :show_x_title => true,
+ # :x_title => "Time",
+ # :stagger_x_labels => true,
+ # :stagger_y_labels => true,
+ # :x_label_format => "%m/%d/%y",
+ # })
+ #
+ # graph.add_data({
+ # :data => data1,
+ # :title => 'Data',
+ # })
+ #
+ # print graph.burn()
+ #
+ # = Description
+ #
+ # Produces a graph of temporal scalar data.
+ #
+ # = Examples
+ #
+ # http://www.germane-software/repositories/public/SVG/test/schedule.rb
+ #
+ # = Notes
+ #
+ # The default stylesheet handles upto 10 data sets, if you
+ # use more you must create your own stylesheet and add the
+ # additional settings for the extra data sets. You will know
+ # if you go over 10 data sets as they will have no style and
+ # be in black.
+ #
+ # Note that multiple data sets within the same chart can differ in
+ # length, and that the data in the datasets needn't be in order;
+ # they will be ordered by the plot along the X-axis.
+ #
+ # The dates must be parseable by ParseDate, but otherwise can be
+ # any order of magnitude (seconds within the hour, or years)
+ #
+ # = See also
+ #
+ # * SVG::Graph::Graph
+ # * SVG::Graph::BarHorizontal
+ # * SVG::Graph::Bar
+ # * SVG::Graph::Line
+ # * SVG::Graph::Pie
+ # * SVG::Graph::Plot
+ # * SVG::Graph::TimeSeries
+ #
+ # == Author
+ #
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
+ #
+ # Copyright 2004 Sean E. Russell
+ # This software is available under the Ruby license[LICENSE.txt]
+ #
+ class Schedule < Graph
+ # In addition to the defaults set by Graph::initialize and
+ # Plot::set_defaults, sets:
+ # [x_label_format] '%Y-%m-%d %H:%M:%S'
+ # [popup_format] '%Y-%m-%d %H:%M:%S'
+ def set_defaults
+ init_with(
+ :x_label_format => '%Y-%m-%d %H:%M:%S',
+ :popup_format => '%Y-%m-%d %H:%M:%S',
+ :scale_x_divisions => false,
+ :scale_x_integers => false,
+ :bar_gap => true
+ )
+ end
+
+ # The format string use do format the X axis labels.
+ # See Time::strformat
+ attr_accessor :x_label_format
+ # Use this to set the spacing between dates on the axis. The value
+ # must be of the form
+ # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
+ #
+ # EG:
+ #
+ # graph.timescale_divisions = "2 weeks"
+ #
+ # will cause the chart to try to divide the X axis up into segments of
+ # two week periods.
+ attr_accessor :timescale_divisions
+ # The formatting used for the popups. See x_label_format
+ attr_accessor :popup_format
+ attr_accessor :min_x_value
+ attr_accessor :scale_x_divisions
+ attr_accessor :scale_x_integers
+ attr_accessor :bar_gap
+
+ # Add data to the plot.
+ #
+ # # A data set with 1 point: Lunch from 12:30 to 14:00
+ # d1 = [ "Lunch", "12:30", "14:00" ]
+ # # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
+ # # "Henry V" runs from 6/12/03 to 8/20/03
+ # d2 = [ "Cats", "5/11/03", "7/15/04",
+ # "Henry V", "6/12/03", "8/20/03" ]
+ #
+ # graph.add_data(
+ # :data => d1,
+ # :title => 'Meetings'
+ # )
+ # graph.add_data(
+ # :data => d2,
+ # :title => 'Plays'
+ # )
+ #
+ # Note that the data must be in time,value pairs, and that the date format
+ # may be any date that is parseable by ParseDate.
+ # Also note that, in this example, we're mixing scales; the data from d1
+ # will probably not be discernable if both data sets are plotted on the same
+ # graph, since d1 is too granular.
+ def add_data data
+ @data = [] unless @data
+
+ raise "No data provided by #{conf.inspect}" unless data[:data] and
+ data[:data].kind_of? Array
+ raise "Data supplied must be title,from,to tripples! "+
+ "The data provided contained an odd set of "+
+ "data points" unless data[:data].length % 3 == 0
+ return if data[:data].length == 0
+
+
+ y = []
+ x_start = []
+ x_end = []
+ data[:data].each_index {|i|
+ im3 = i%3
+ if im3 == 0
+ y << data[:data][i]
+ else
+ arr = ParseDate.parsedate( data[:data][i] )
+ t = Time.local( *arr[0,6].compact )
+ (im3 == 1 ? x_start : x_end) << t.to_i
+ end
+ }
+ sort( x_start, x_end, y )
+ @data = [x_start, x_end, y ]
+ end
+
+
+ protected
+
+ def min_x_value=(value)
+ arr = ParseDate.parsedate( value )
+ @min_x_value = Time.local( *arr[0,6].compact ).to_i
+ end
+
+
+ def format x, y
+ Time.at( x ).strftime( popup_format )
+ end
+
+ def get_x_labels
+ rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
+ end
+
+ def y_label_offset( height )
+ height / -2.0
+ end
+
+ def get_y_labels
+ @data[2]
+ end
+
+ def draw_data
+ fieldheight = field_height
+ fieldwidth = field_width
+
+ bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
+ subbar_height = fieldheight - bargap
+
+ field_count = 1
+ y_mod = (subbar_height / 2) + (font_size / 2)
+ min,max,div = x_range
+ scale = (@graph_width.to_f - font_size*2) / (max-min)
+ @data[0].each_index { |i|
+ x_start = @data[0][i]
+ x_end = @data[1][i]
+ y = @graph_height - (fieldheight * field_count)
+ bar_width = (x_end-x_start) * scale
+ bar_start = x_start * scale - (min * scale)
+
+ @graph.add_element( "rect", {
+ "x" => bar_start.to_s,
+ "y" => y.to_s,
+ "width" => bar_width.to_s,
+ "height" => subbar_height.to_s,
+ "class" => "fill#{field_count+1}"
+ })
+ field_count += 1
+ }
+ end
+
+ def get_css
+ return <<EOL
+/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
+.key1,.fill1{
+ fill: #ff0000;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 0.5px;
+}
+.key2,.fill2{
+ fill: #0000ff;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key3,.fill3{
+ fill: #00ff00;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key4,.fill4{
+ fill: #ffcc00;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key5,.fill5{
+ fill: #00ccff;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key6,.fill6{
+ fill: #ff00ff;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key7,.fill7{
+ fill: #00ffff;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key8,.fill8{
+ fill: #ffff00;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key9,.fill9{
+ fill: #cc6666;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key10,.fill10{
+ fill: #663399;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key11,.fill11{
+ fill: #339900;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+.key12,.fill12{
+ fill: #9966FF;
+ fill-opacity: 0.5;
+ stroke: none;
+ stroke-width: 1px;
+}
+EOL
+ end
+
+ private
+ def x_range
+ max_value = [ @data[0][-1], @data[1].max ].max
+ min_value = [ @data[0][0], @data[1].min ].min
+ min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
+
+ range = max_value - min_value
+ right_pad = range == 0 ? 10 : range / 20.0
+ scale_range = (max_value + right_pad) - min_value
+
+ scale_division = scale_x_divisions || (scale_range / 10.0)
+
+ if scale_x_integers
+ scale_division = scale_division < 1 ? 1 : scale_division.round
+ end
+
+ [min_value, max_value, scale_division]
+ end
+
+ def get_x_values
+ rv = []
+ min, max, scale_division = x_range
+ if timescale_divisions
+ timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
+ division_units = $2 ? $2 : "days"
+ amount = $1.to_i
+ if amount
+ step = nil
+ case division_units
+ when "months"
+ cur = min
+ while cur < max
+ rv << cur
+ arr = Time.at( cur ).to_a
+ arr[4] += amount
+ if arr[4] > 12
+ arr[5] += (arr[4] / 12).to_i
+ arr[4] = (arr[4] % 12)
+ end
+ cur = Time.local(*arr).to_i
+ end
+ when "years"
+ cur = min
+ while cur < max
+ rv << cur
+ arr = Time.at( cur ).to_a
+ arr[5] += amount
+ cur = Time.local(*arr).to_i
+ end
+ when "weeks"
+ step = 7 * 24 * 60 * 60 * amount
+ when "days"
+ step = 24 * 60 * 60 * amount
+ when "hours"
+ step = 60 * 60 * amount
+ when "minutes"
+ step = 60 * amount
+ when "seconds"
+ step = amount
+ end
+ min.step( max, step ) {|v| rv << v} if step
+
+ return rv
+ end
+ end
+ min.step( max, scale_division ) {|v| rv << v}
+ return rv
+ end
+ end
+ end
+end
--- /dev/null
+require 'SVG/Graph/Plot'\r
+require 'parsedate'\r
+\r
+module SVG\r
+ module Graph\r
+ # === For creating SVG plots of scalar temporal data\r
+ # \r
+ # = Synopsis\r
+ # \r
+ # require 'SVG/Graph/TimeSeriess'\r
+ # \r
+ # # Data sets are x,y pairs\r
+ # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, \r
+ # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]\r
+ # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, \r
+ # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, \r
+ # "5/1/84", 17, "10/1/80", 12]\r
+ #\r
+ # graph = SVG::Graph::TimeSeries.new( {\r
+ # :width => 640,\r
+ # :height => 480,\r
+ # :graph_title => title,\r
+ # :show_graph_title => true,\r
+ # :no_css => true,\r
+ # :key => true,\r
+ # :scale_x_integers => true,\r
+ # :scale_y_integers => true,\r
+ # :min_x_value => 0,\r
+ # :min_y_value => 0,\r
+ # :show_data_labels => true,\r
+ # :show_x_guidelines => true,\r
+ # :show_x_title => true,\r
+ # :x_title => "Time",\r
+ # :show_y_title => true,\r
+ # :y_title => "Ice Cream Cones",\r
+ # :y_title_text_direction => :bt,\r
+ # :stagger_x_labels => true,\r
+ # :x_label_format => "%m/%d/%y",\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => projection\r
+ # :title => 'Projected',\r
+ # })\r
+ # \r
+ # graph.add_data({\r
+ # :data => actual,\r
+ # :title => 'Actual',\r
+ # })\r
+ # \r
+ # print graph.burn()\r
+ #\r
+ # = Description\r
+ # \r
+ # Produces a graph of temporal scalar data.\r
+ # \r
+ # = Examples\r
+ #\r
+ # http://www.germane-software/repositories/public/SVG/test/timeseries.rb\r
+ # \r
+ # = Notes\r
+ # \r
+ # The default stylesheet handles upto 10 data sets, if you\r
+ # use more you must create your own stylesheet and add the\r
+ # additional settings for the extra data sets. You will know\r
+ # if you go over 10 data sets as they will have no style and\r
+ # be in black.\r
+ #\r
+ # Unlike the other types of charts, data sets must contain x,y pairs:\r
+ #\r
+ # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)\r
+ # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and \r
+ # # ("14:20",6) \r
+ #\r
+ # Note that multiple data sets within the same chart can differ in length, \r
+ # and that the data in the datasets needn't be in order; they will be ordered\r
+ # by the plot along the X-axis.\r
+ # \r
+ # The dates must be parseable by ParseDate, but otherwise can be\r
+ # any order of magnitude (seconds within the hour, or years)\r
+ # \r
+ # = See also\r
+ # \r
+ # * SVG::Graph::Graph\r
+ # * SVG::Graph::BarHorizontal\r
+ # * SVG::Graph::Bar\r
+ # * SVG::Graph::Line\r
+ # * SVG::Graph::Pie\r
+ # * SVG::Graph::Plot\r
+ #\r
+ # == Author\r
+ #\r
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>\r
+ #\r
+ # Copyright 2004 Sean E. Russell\r
+ # This software is available under the Ruby license[LICENSE.txt]\r
+ #\r
+ class TimeSeries < Plot\r
+ # In addition to the defaults set by Graph::initialize and\r
+ # Plot::set_defaults, sets:\r
+ # [x_label_format] '%Y-%m-%d %H:%M:%S'\r
+ # [popup_format] '%Y-%m-%d %H:%M:%S'\r
+ def set_defaults\r
+ super\r
+ init_with(\r
+ #:max_time_span => '',\r
+ :x_label_format => '%Y-%m-%d %H:%M:%S',\r
+ :popup_format => '%Y-%m-%d %H:%M:%S'\r
+ )\r
+ end\r
+\r
+ # The format string use do format the X axis labels.\r
+ # See Time::strformat\r
+ attr_accessor :x_label_format\r
+ # Use this to set the spacing between dates on the axis. The value\r
+ # must be of the form \r
+ # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"\r
+ # \r
+ # EG:\r
+ #\r
+ # graph.timescale_divisions = "2 weeks"\r
+ #\r
+ # will cause the chart to try to divide the X axis up into segments of\r
+ # two week periods.\r
+ attr_accessor :timescale_divisions\r
+ # The formatting used for the popups. See x_label_format\r
+ attr_accessor :popup_format\r
+\r
+ # Add data to the plot.\r
+ #\r
+ # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)\r
+ # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and \r
+ # # ("14:20",6) \r
+ # graph.add_data( \r
+ # :data => d1,\r
+ # :title => 'One'\r
+ # )\r
+ # graph.add_data(\r
+ # :data => d2,\r
+ # :title => 'Two'\r
+ # )\r
+ #\r
+ # Note that the data must be in time,value pairs, and that the date format\r
+ # may be any date that is parseable by ParseDate.\r
+ def add_data data\r
+ @data = [] unless @data\r
+ \r
+ raise "No data provided by #{@data.inspect}" unless data[:data] and\r
+ data[:data].kind_of? Array\r
+ raise "Data supplied must be x,y pairs! "+\r
+ "The data provided contained an odd set of "+\r
+ "data points" unless data[:data].length % 2 == 0\r
+ return if data[:data].length == 0\r
+\r
+\r
+ x = []\r
+ y = []\r
+ data[:data].each_index {|i|\r
+ if i%2 == 0\r
+ arr = ParseDate.parsedate( data[:data][i] )\r
+ t = Time.local( *arr[0,6].compact )\r
+ x << t.to_i\r
+ else\r
+ y << data[:data][i]\r
+ end\r
+ }\r
+ sort( x, y )\r
+ data[:data] = [x,y]\r
+ @data << data\r
+ end\r
+\r
+\r
+ protected\r
+\r
+ def min_x_value=(value)\r
+ arr = ParseDate.parsedate( value )\r
+ @min_x_value = Time.local( *arr[0,6].compact ).to_i\r
+ end\r
+\r
+\r
+ def format x, y\r
+ Time.at( x ).strftime( popup_format )\r
+ end\r
+\r
+ def get_x_labels\r
+ get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }\r
+ end\r
+ \r
+ private\r
+ def get_x_values\r
+ rv = []\r
+ min, max, scale_division = x_range\r
+ if timescale_divisions\r
+ timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/\r
+ division_units = $2 ? $2 : "day"\r
+ amount = $1.to_i\r
+ if amount\r
+ step = nil\r
+ case division_units\r
+ when "month"\r
+ cur = min\r
+ while cur < max\r
+ rv << cur\r
+ arr = Time.at( cur ).to_a\r
+ arr[4] += amount\r
+ if arr[4] > 12\r
+ arr[5] += (arr[4] / 12).to_i\r
+ arr[4] = (arr[4] % 12)\r
+ end\r
+ cur = Time.local(*arr).to_i\r
+ end\r
+ when "year"\r
+ cur = min\r
+ while cur < max\r
+ rv << cur\r
+ arr = Time.at( cur ).to_a\r
+ arr[5] += amount\r
+ cur = Time.local(*arr).to_i\r
+ end\r
+ when "week"\r
+ step = 7 * 24 * 60 * 60 * amount\r
+ when "day"\r
+ step = 24 * 60 * 60 * amount\r
+ when "hour"\r
+ step = 60 * 60 * amount\r
+ when "minute"\r
+ step = 60 * amount\r
+ when "second"\r
+ step = amount\r
+ end\r
+ min.step( max, step ) {|v| rv << v} if step\r
+\r
+ return rv\r
+ end\r
+ end\r
+ min.step( max, scale_division ) {|v| rv << v}\r
+ return rv\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+SVG::Graph is copyrighted free software by Sean Russell <ser@germane-software.com>.
+You can redistribute it and/or modify it under either the terms of the GPL
+(see GPL.txt file), or the conditions below:
+
+ 1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+ 2. You may modify your copy of the software in any way, provided that
+ you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise
+ make them Freely Available, such as by posting said
+ modifications to Usenet or an equivalent medium, or by allowing
+ the author to include your modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) rename any non-standard executables so the names do not conflict
+ with standard executables, which must also be provided.
+
+ d) make other distribution arrangements with the author.
+
+ 3. You may distribute the software in object code or executable
+ form, provided that you do at least ONE of the following:
+
+ a) distribute the executables and library files of the software,
+ together with instructions (in the manual page or equivalent)
+ on where to get the original distribution.
+
+ b) accompany the distribution with the machine-readable source of
+ the software.
+
+ c) give non-standard executables non-standard names, with
+ instructions on where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+ 4. You may modify and include the part of the software into any other
+ software (possibly commercial). But some files in the distribution
+ are not written by the author, so that they are not under this terms.
+
+ All files of this sort are located under the contrib/ directory.
+ See each file for the copying condition.
+
+ 5. The scripts and library files supplied as input to or produced as
+ output from the software do not automatically fall under the
+ copyright of the software, but belong to whomever generated them,
+ and may be sold commercially, and may be aggregated with this
+ software.
+
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE.
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 ARCondition
+ attr_reader :conditions
+
+ def initialize(condition=nil)
+ @conditions = ['1=1']
+ add(condition) if condition
+ end
+
+ def add(condition)
+ if condition.is_a?(Array)
+ @conditions.first << " AND (#{condition.first})"
+ @conditions += condition[1..-1]
+ elsif condition.is_a?(String)
+ @conditions.first << " AND (#{condition})"
+ else
+ raise "Unsupported #{condition.class} condition: #{condition}"
+ end
+ self
+ end
+
+ def <<(condition)
+ add(condition)
+ end
+end
--- /dev/null
+module RedmineDiff
+ class Diff
+
+ VERSION = 0.3
+
+ def Diff.lcs(a, b)
+ astart = 0
+ bstart = 0
+ afinish = a.length-1
+ bfinish = b.length-1
+ mvector = []
+
+ # First we prune off any common elements at the beginning
+ while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart])
+ mvector[astart] = bstart
+ astart += 1
+ bstart += 1
+ end
+
+ # now the end
+ while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish])
+ mvector[afinish] = bfinish
+ afinish -= 1
+ bfinish -= 1
+ end
+
+ bmatches = b.reverse_hash(bstart..bfinish)
+ thresh = []
+ links = []
+
+ (astart..afinish).each { |aindex|
+ aelem = a[aindex]
+ next unless bmatches.has_key? aelem
+ k = nil
+ bmatches[aelem].reverse.each { |bindex|
+ if k && (thresh[k] > bindex) && (thresh[k-1] < bindex)
+ thresh[k] = bindex
+ else
+ k = thresh.replacenextlarger(bindex, k)
+ end
+ links[k] = [ (k==0) ? nil : links[k-1], aindex, bindex ] if k
+ }
+ }
+
+ if !thresh.empty?
+ link = links[thresh.length-1]
+ while link
+ mvector[link[1]] = link[2]
+ link = link[0]
+ end
+ end
+
+ return mvector
+ end
+
+ def makediff(a, b)
+ mvector = Diff.lcs(a, b)
+ ai = bi = 0
+ while ai < mvector.length
+ bline = mvector[ai]
+ if bline
+ while bi < bline
+ discardb(bi, b[bi])
+ bi += 1
+ end
+ match(ai, bi)
+ bi += 1
+ else
+ discarda(ai, a[ai])
+ end
+ ai += 1
+ end
+ while ai < a.length
+ discarda(ai, a[ai])
+ ai += 1
+ end
+ while bi < b.length
+ discardb(bi, b[bi])
+ bi += 1
+ end
+ match(ai, bi)
+ 1
+ end
+
+ def compactdiffs
+ diffs = []
+ @diffs.each { |df|
+ i = 0
+ curdiff = []
+ while i < df.length
+ whot = df[i][0]
+ s = @isstring ? df[i][2].chr : [df[i][2]]
+ p = df[i][1]
+ last = df[i][1]
+ i += 1
+ while df[i] && df[i][0] == whot && df[i][1] == last+1
+ s << df[i][2]
+ last = df[i][1]
+ i += 1
+ end
+ curdiff.push [whot, p, s]
+ end
+ diffs.push curdiff
+ }
+ return diffs
+ end
+
+ attr_reader :diffs, :difftype
+
+ def initialize(diffs_or_a, b = nil, isstring = nil)
+ if b.nil?
+ @diffs = diffs_or_a
+ @isstring = isstring
+ else
+ @diffs = []
+ @curdiffs = []
+ makediff(diffs_or_a, b)
+ @difftype = diffs_or_a.class
+ end
+ end
+
+ def match(ai, bi)
+ @diffs.push @curdiffs unless @curdiffs.empty?
+ @curdiffs = []
+ end
+
+ def discarda(i, elem)
+ @curdiffs.push ['-', i, elem]
+ end
+
+ def discardb(i, elem)
+ @curdiffs.push ['+', i, elem]
+ end
+
+ def compact
+ return Diff.new(compactdiffs)
+ end
+
+ def compact!
+ @diffs = compactdiffs
+ end
+
+ def inspect
+ @diffs.inspect
+ end
+
+ end
+end
+
+module Diffable
+ def diff(b)
+ RedmineDiff::Diff.new(self, b)
+ end
+
+ # Create a hash that maps elements of the array to arrays of indices
+ # where the elements are found.
+
+ def reverse_hash(range = (0...self.length))
+ revmap = {}
+ range.each { |i|
+ elem = self[i]
+ if revmap.has_key? elem
+ revmap[elem].push i
+ else
+ revmap[elem] = [i]
+ end
+ }
+ return revmap
+ end
+
+ def replacenextlarger(value, high = nil)
+ high ||= self.length
+ if self.empty? || value > self[-1]
+ push value
+ return high
+ end
+ # binary search for replacement point
+ low = 0
+ while low < high
+ index = (high+low)/2
+ found = self[index]
+ return nil if value == found
+ if value > found
+ low = index + 1
+ else
+ high = index
+ end
+ end
+
+ self[low] = value
+ # $stderr << "replace #{value} : 0/#{low}/#{init_high} (#{steps} steps) (#{init_high-low} off )\n"
+ # $stderr.puts self.inspect
+ #gets
+ #p length - low
+ return low
+ end
+
+ def patch(diff)
+ newary = nil
+ if diff.difftype == String
+ newary = diff.difftype.new('')
+ else
+ newary = diff.difftype.new
+ end
+ ai = 0
+ bi = 0
+ diff.diffs.each { |d|
+ d.each { |mod|
+ case mod[0]
+ when '-'
+ while ai < mod[1]
+ newary << self[ai]
+ ai += 1
+ bi += 1
+ end
+ ai += 1
+ when '+'
+ while bi < mod[1]
+ newary << self[ai]
+ ai += 1
+ bi += 1
+ end
+ newary << mod[2]
+ bi += 1
+ else
+ raise "Unknown diff action"
+ end
+ }
+ }
+ while ai < self.length
+ newary << self[ai]
+ ai += 1
+ bi += 1
+ end
+ return newary
+ end
+end
+
+class Array
+ include Diffable
+end
+
+class String
+ include Diffable
+end
+
+=begin
+ = Diff
+ (({diff.rb})) - computes the differences between two arrays or
+ strings. Copyright (C) 2001 Lars Christensen
+
+ == Synopsis
+
+ diff = Diff.new(a, b)
+ b = a.patch(diff)
+
+ == Class Diff
+ === Class Methods
+ --- Diff.new(a, b)
+ --- a.diff(b)
+ Creates a Diff object which represent the differences between
+ ((|a|)) and ((|b|)). ((|a|)) and ((|b|)) can be either be arrays
+ of any objects, strings, or object of any class that include
+ module ((|Diffable|))
+
+ == Module Diffable
+ The module ((|Diffable|)) is intended to be included in any class for
+ which differences are to be computed. Diffable is included into String
+ and Array when (({diff.rb})) is (({require}))'d.
+
+ Classes including Diffable should implement (({[]})) to get element at
+ integer indices, (({<<})) to append elements to the object and
+ (({ClassName#new})) should accept 0 arguments to create a new empty
+ object.
+
+ === Instance Methods
+ --- Diffable#patch(diff)
+ Applies the differences from ((|diff|)) to the object ((|obj|))
+ and return the result. ((|obj|)) is not changed. ((|obj|)) and
+ can be either an array or a string, but must match the object
+ from which the ((|diff|)) was created.
+=end
--- /dev/null
+#!/usr/local/bin/ruby -w\r
+\r
+# = faster_csv.rb -- Faster CSV Reading and Writing\r
+#\r
+# Created by James Edward Gray II on 2005-10-31.\r
+# Copyright 2005 Gray Productions. All rights reserved.\r
+# \r
+# See FasterCSV for documentation.\r
+\r
+if RUBY_VERSION >= "1.9"\r
+ abort <<-VERSION_WARNING.gsub(/^\s+/, "")\r
+ Please switch to Ruby 1.9's standard CSV library. It's FasterCSV plus\r
+ support for Ruby 1.9's m17n encoding engine.\r
+ VERSION_WARNING\r
+end\r
+\r
+require "forwardable"\r
+require "English"\r
+require "enumerator"\r
+require "date"\r
+require "stringio"\r
+\r
+# \r
+# This class provides a complete interface to CSV files and data. It offers\r
+# tools to enable you to read and write to and from Strings or IO objects, as\r
+# needed.\r
+# \r
+# == Reading\r
+# \r
+# === From a File\r
+# \r
+# ==== A Line at a Time\r
+# \r
+# FasterCSV.foreach("path/to/file.csv") do |row|\r
+# # use row here...\r
+# end\r
+# \r
+# ==== All at Once\r
+# \r
+# arr_of_arrs = FasterCSV.read("path/to/file.csv")\r
+# \r
+# === From a String\r
+# \r
+# ==== A Line at a Time\r
+# \r
+# FasterCSV.parse("CSV,data,String") do |row|\r
+# # use row here...\r
+# end\r
+# \r
+# ==== All at Once\r
+# \r
+# arr_of_arrs = FasterCSV.parse("CSV,data,String")\r
+# \r
+# == Writing\r
+# \r
+# === To a File\r
+# \r
+# FasterCSV.open("path/to/file.csv", "w") do |csv|\r
+# csv << ["row", "of", "CSV", "data"]\r
+# csv << ["another", "row"]\r
+# # ...\r
+# end\r
+# \r
+# === To a String\r
+# \r
+# csv_string = FasterCSV.generate do |csv|\r
+# csv << ["row", "of", "CSV", "data"]\r
+# csv << ["another", "row"]\r
+# # ...\r
+# end\r
+# \r
+# == Convert a Single Line\r
+# \r
+# csv_string = ["CSV", "data"].to_csv # to CSV\r
+# csv_array = "CSV,String".parse_csv # from CSV\r
+# \r
+# == Shortcut Interface\r
+# \r
+# FCSV { |csv_out| csv_out << %w{my data here} } # to $stdout\r
+# FCSV(csv = "") { |csv_str| csv_str << %w{my data here} } # to a String\r
+# FCSV($stderr) { |csv_err| csv_err << %w{my data here} } # to $stderr\r
+# \r
+class FasterCSV\r
+ # The version of the installed library.\r
+ VERSION = "1.5.0".freeze\r
+ \r
+ # \r
+ # A FasterCSV::Row is part Array and part Hash. It retains an order for the\r
+ # fields and allows duplicates just as an Array would, but also allows you to\r
+ # access fields by name just as you could if they were in a Hash.\r
+ # \r
+ # All rows returned by FasterCSV will be constructed from this class, if\r
+ # header row processing is activated.\r
+ # \r
+ class Row\r
+ # \r
+ # Construct a new FasterCSV::Row from +headers+ and +fields+, which are\r
+ # expected to be Arrays. If one Array is shorter than the other, it will be\r
+ # padded with +nil+ objects.\r
+ # \r
+ # The optional +header_row+ parameter can be set to +true+ to indicate, via\r
+ # FasterCSV::Row.header_row?() and FasterCSV::Row.field_row?(), that this is\r
+ # a header row. Otherwise, the row is assumes to be a field row.\r
+ # \r
+ # A FasterCSV::Row object supports the following Array methods through\r
+ # delegation:\r
+ # \r
+ # * empty?()\r
+ # * length()\r
+ # * size()\r
+ # \r
+ def initialize(headers, fields, header_row = false)\r
+ @header_row = header_row\r
+ \r
+ # handle extra headers or fields\r
+ @row = if headers.size > fields.size\r
+ headers.zip(fields)\r
+ else\r
+ fields.zip(headers).map { |pair| pair.reverse }\r
+ end\r
+ end\r
+ \r
+ # Internal data format used to compare equality.\r
+ attr_reader :row\r
+ protected :row\r
+\r
+ ### Array Delegation ###\r
+\r
+ extend Forwardable\r
+ def_delegators :@row, :empty?, :length, :size\r
+ \r
+ # Returns +true+ if this is a header row.\r
+ def header_row?\r
+ @header_row\r
+ end\r
+ \r
+ # Returns +true+ if this is a field row.\r
+ def field_row?\r
+ not header_row?\r
+ end\r
+ \r
+ # Returns the headers of this row.\r
+ def headers\r
+ @row.map { |pair| pair.first }\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # field( header )\r
+ # field( header, offset )\r
+ # field( index )\r
+ # \r
+ # This method will fetch the field value by +header+ or +index+. If a field\r
+ # is not found, +nil+ is returned.\r
+ # \r
+ # When provided, +offset+ ensures that a header match occurrs on or later\r
+ # than the +offset+ index. You can use this to find duplicate headers, \r
+ # without resorting to hard-coding exact indices.\r
+ # \r
+ def field(header_or_index, minimum_index = 0)\r
+ # locate the pair\r
+ finder = header_or_index.is_a?(Integer) ? :[] : :assoc\r
+ pair = @row[minimum_index..-1].send(finder, header_or_index)\r
+\r
+ # return the field if we have a pair\r
+ pair.nil? ? nil : pair.last\r
+ end\r
+ alias_method :[], :field\r
+ \r
+ # \r
+ # :call-seq:\r
+ # []=( header, value )\r
+ # []=( header, offset, value )\r
+ # []=( index, value )\r
+ # \r
+ # Looks up the field by the semantics described in FasterCSV::Row.field()\r
+ # and assigns the +value+.\r
+ # \r
+ # Assigning past the end of the row with an index will set all pairs between\r
+ # to <tt>[nil, nil]</tt>. Assigning to an unused header appends the new\r
+ # pair.\r
+ # \r
+ def []=(*args)\r
+ value = args.pop\r
+ \r
+ if args.first.is_a? Integer\r
+ if @row[args.first].nil? # extending past the end with index\r
+ @row[args.first] = [nil, value]\r
+ @row.map! { |pair| pair.nil? ? [nil, nil] : pair }\r
+ else # normal index assignment\r
+ @row[args.first][1] = value\r
+ end\r
+ else\r
+ index = index(*args)\r
+ if index.nil? # appending a field\r
+ self << [args.first, value]\r
+ else # normal header assignment\r
+ @row[index][1] = value\r
+ end\r
+ end\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # <<( field )\r
+ # <<( header_and_field_array )\r
+ # <<( header_and_field_hash )\r
+ # \r
+ # If a two-element Array is provided, it is assumed to be a header and field\r
+ # and the pair is appended. A Hash works the same way with the key being\r
+ # the header and the value being the field. Anything else is assumed to be\r
+ # a lone field which is appended with a +nil+ header.\r
+ # \r
+ # This method returns the row for chaining.\r
+ # \r
+ def <<(arg)\r
+ if arg.is_a?(Array) and arg.size == 2 # appending a header and name\r
+ @row << arg\r
+ elsif arg.is_a?(Hash) # append header and name pairs\r
+ arg.each { |pair| @row << pair }\r
+ else # append field value\r
+ @row << [nil, arg]\r
+ end\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # \r
+ # A shortcut for appending multiple fields. Equivalent to:\r
+ # \r
+ # args.each { |arg| faster_csv_row << arg }\r
+ # \r
+ # This method returns the row for chaining.\r
+ # \r
+ def push(*args)\r
+ args.each { |arg| self << arg }\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # delete( header )\r
+ # delete( header, offset )\r
+ # delete( index )\r
+ # \r
+ # Used to remove a pair from the row by +header+ or +index+. The pair is\r
+ # located as described in FasterCSV::Row.field(). The deleted pair is \r
+ # returned, or +nil+ if a pair could not be found.\r
+ # \r
+ def delete(header_or_index, minimum_index = 0)\r
+ if header_or_index.is_a? Integer # by index\r
+ @row.delete_at(header_or_index)\r
+ else # by header\r
+ @row.delete_at(index(header_or_index, minimum_index))\r
+ end\r
+ end\r
+ \r
+ # \r
+ # The provided +block+ is passed a header and field for each pair in the row\r
+ # and expected to return +true+ or +false+, depending on whether the pair\r
+ # should be deleted.\r
+ # \r
+ # This method returns the row for chaining.\r
+ # \r
+ def delete_if(&block)\r
+ @row.delete_if(&block)\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # \r
+ # This method accepts any number of arguments which can be headers, indices,\r
+ # Ranges of either, or two-element Arrays containing a header and offset. \r
+ # Each argument will be replaced with a field lookup as described in\r
+ # FasterCSV::Row.field().\r
+ # \r
+ # If called with no arguments, all fields are returned.\r
+ # \r
+ def fields(*headers_and_or_indices)\r
+ if headers_and_or_indices.empty? # return all fields--no arguments\r
+ @row.map { |pair| pair.last }\r
+ else # or work like values_at()\r
+ headers_and_or_indices.inject(Array.new) do |all, h_or_i|\r
+ all + if h_or_i.is_a? Range\r
+ index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin :\r
+ index(h_or_i.begin)\r
+ index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end :\r
+ index(h_or_i.end)\r
+ new_range = h_or_i.exclude_end? ? (index_begin...index_end) :\r
+ (index_begin..index_end)\r
+ fields.values_at(new_range)\r
+ else\r
+ [field(*Array(h_or_i))]\r
+ end\r
+ end\r
+ end\r
+ end\r
+ alias_method :values_at, :fields\r
+ \r
+ # \r
+ # :call-seq:\r
+ # index( header )\r
+ # index( header, offset )\r
+ # \r
+ # This method will return the index of a field with the provided +header+.\r
+ # The +offset+ can be used to locate duplicate header names, as described in\r
+ # FasterCSV::Row.field().\r
+ # \r
+ def index(header, minimum_index = 0)\r
+ # find the pair\r
+ index = headers[minimum_index..-1].index(header)\r
+ # return the index at the right offset, if we found one\r
+ index.nil? ? nil : index + minimum_index\r
+ end\r
+ \r
+ # Returns +true+ if +name+ is a header for this row, and +false+ otherwise.\r
+ def header?(name)\r
+ headers.include? name\r
+ end\r
+ alias_method :include?, :header?\r
+ \r
+ # \r
+ # Returns +true+ if +data+ matches a field in this row, and +false+\r
+ # otherwise.\r
+ # \r
+ def field?(data)\r
+ fields.include? data\r
+ end\r
+\r
+ include Enumerable\r
+ \r
+ # \r
+ # Yields each pair of the row as header and field tuples (much like\r
+ # iterating over a Hash).\r
+ # \r
+ # Support for Enumerable.\r
+ # \r
+ # This method returns the row for chaining.\r
+ # \r
+ def each(&block)\r
+ @row.each(&block)\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # \r
+ # Returns +true+ if this row contains the same headers and fields in the \r
+ # same order as +other+.\r
+ # \r
+ def ==(other)\r
+ @row == other.row\r
+ end\r
+ \r
+ # \r
+ # Collapses the row into a simple Hash. Be warning that this discards field\r
+ # order and clobbers duplicate fields.\r
+ # \r
+ def to_hash\r
+ # flatten just one level of the internal Array\r
+ Hash[*@row.inject(Array.new) { |ary, pair| ary.push(*pair) }]\r
+ end\r
+ \r
+ # \r
+ # Returns the row as a CSV String. Headers are not used. Equivalent to:\r
+ # \r
+ # faster_csv_row.fields.to_csv( options )\r
+ # \r
+ def to_csv(options = Hash.new)\r
+ fields.to_csv(options)\r
+ end\r
+ alias_method :to_s, :to_csv\r
+ \r
+ # A summary of fields, by header.\r
+ def inspect\r
+ str = "#<#{self.class}"\r
+ each do |header, field|\r
+ str << " #{header.is_a?(Symbol) ? header.to_s : header.inspect}:" <<\r
+ field.inspect\r
+ end\r
+ str << ">"\r
+ end\r
+ end\r
+ \r
+ # \r
+ # A FasterCSV::Table is a two-dimensional data structure for representing CSV\r
+ # documents. Tables allow you to work with the data by row or column, \r
+ # manipulate the data, and even convert the results back to CSV, if needed.\r
+ # \r
+ # All tables returned by FasterCSV will be constructed from this class, if\r
+ # header row processing is activated.\r
+ # \r
+ class Table\r
+ # \r
+ # Construct a new FasterCSV::Table from +array_of_rows+, which are expected\r
+ # to be FasterCSV::Row objects. All rows are assumed to have the same \r
+ # headers.\r
+ # \r
+ # A FasterCSV::Table object supports the following Array methods through\r
+ # delegation:\r
+ # \r
+ # * empty?()\r
+ # * length()\r
+ # * size()\r
+ # \r
+ def initialize(array_of_rows)\r
+ @table = array_of_rows\r
+ @mode = :col_or_row\r
+ end\r
+ \r
+ # The current access mode for indexing and iteration.\r
+ attr_reader :mode\r
+ \r
+ # Internal data format used to compare equality.\r
+ attr_reader :table\r
+ protected :table\r
+\r
+ ### Array Delegation ###\r
+\r
+ extend Forwardable\r
+ def_delegators :@table, :empty?, :length, :size\r
+ \r
+ # \r
+ # Returns a duplicate table object, in column mode. This is handy for \r
+ # chaining in a single call without changing the table mode, but be aware \r
+ # that this method can consume a fair amount of memory for bigger data sets.\r
+ # \r
+ # This method returns the duplicate table for chaining. Don't chain\r
+ # destructive methods (like []=()) this way though, since you are working\r
+ # with a duplicate.\r
+ # \r
+ def by_col\r
+ self.class.new(@table.dup).by_col!\r
+ end\r
+ \r
+ # \r
+ # Switches the mode of this table to column mode. All calls to indexing and\r
+ # iteration methods will work with columns until the mode is changed again.\r
+ # \r
+ # This method returns the table and is safe to chain.\r
+ # \r
+ def by_col!\r
+ @mode = :col\r
+ \r
+ self\r
+ end\r
+ \r
+ # \r
+ # Returns a duplicate table object, in mixed mode. This is handy for \r
+ # chaining in a single call without changing the table mode, but be aware \r
+ # that this method can consume a fair amount of memory for bigger data sets.\r
+ # \r
+ # This method returns the duplicate table for chaining. Don't chain\r
+ # destructive methods (like []=()) this way though, since you are working\r
+ # with a duplicate.\r
+ # \r
+ def by_col_or_row\r
+ self.class.new(@table.dup).by_col_or_row!\r
+ end\r
+ \r
+ # \r
+ # Switches the mode of this table to mixed mode. All calls to indexing and\r
+ # iteration methods will use the default intelligent indexing system until\r
+ # the mode is changed again. In mixed mode an index is assumed to be a row\r
+ # reference while anything else is assumed to be column access by headers.\r
+ # \r
+ # This method returns the table and is safe to chain.\r
+ # \r
+ def by_col_or_row!\r
+ @mode = :col_or_row\r
+ \r
+ self\r
+ end\r
+ \r
+ # \r
+ # Returns a duplicate table object, in row mode. This is handy for chaining\r
+ # in a single call without changing the table mode, but be aware that this\r
+ # method can consume a fair amount of memory for bigger data sets.\r
+ # \r
+ # This method returns the duplicate table for chaining. Don't chain\r
+ # destructive methods (like []=()) this way though, since you are working\r
+ # with a duplicate.\r
+ # \r
+ def by_row\r
+ self.class.new(@table.dup).by_row!\r
+ end\r
+ \r
+ # \r
+ # Switches the mode of this table to row mode. All calls to indexing and\r
+ # iteration methods will work with rows until the mode is changed again.\r
+ # \r
+ # This method returns the table and is safe to chain.\r
+ # \r
+ def by_row!\r
+ @mode = :row\r
+ \r
+ self\r
+ end\r
+ \r
+ # \r
+ # Returns the headers for the first row of this table (assumed to match all\r
+ # other rows). An empty Array is returned for empty tables.\r
+ # \r
+ def headers\r
+ if @table.empty?\r
+ Array.new\r
+ else\r
+ @table.first.headers\r
+ end\r
+ end\r
+ \r
+ # \r
+ # In the default mixed mode, this method returns rows for index access and\r
+ # columns for header access. You can force the index association by first\r
+ # calling by_col!() or by_row!().\r
+ # \r
+ # Columns are returned as an Array of values. Altering that Array has no\r
+ # effect on the table.\r
+ # \r
+ def [](index_or_header)\r
+ if @mode == :row or # by index\r
+ (@mode == :col_or_row and index_or_header.is_a? Integer)\r
+ @table[index_or_header]\r
+ else # by header\r
+ @table.map { |row| row[index_or_header] }\r
+ end\r
+ end\r
+ \r
+ # \r
+ # In the default mixed mode, this method assigns rows for index access and\r
+ # columns for header access. You can force the index association by first\r
+ # calling by_col!() or by_row!().\r
+ # \r
+ # Rows may be set to an Array of values (which will inherit the table's\r
+ # headers()) or a FasterCSV::Row.\r
+ # \r
+ # Columns may be set to a single value, which is copied to each row of the \r
+ # column, or an Array of values. Arrays of values are assigned to rows top\r
+ # to bottom in row major order. Excess values are ignored and if the Array\r
+ # does not have a value for each row the extra rows will receive a +nil+.\r
+ # \r
+ # Assigning to an existing column or row clobbers the data. Assigning to\r
+ # new columns creates them at the right end of the table.\r
+ # \r
+ def []=(index_or_header, value)\r
+ if @mode == :row or # by index\r
+ (@mode == :col_or_row and index_or_header.is_a? Integer)\r
+ if value.is_a? Array\r
+ @table[index_or_header] = Row.new(headers, value)\r
+ else\r
+ @table[index_or_header] = value\r
+ end\r
+ else # set column\r
+ if value.is_a? Array # multiple values\r
+ @table.each_with_index do |row, i|\r
+ if row.header_row?\r
+ row[index_or_header] = index_or_header\r
+ else\r
+ row[index_or_header] = value[i]\r
+ end\r
+ end\r
+ else # repeated value\r
+ @table.each do |row|\r
+ if row.header_row?\r
+ row[index_or_header] = index_or_header\r
+ else\r
+ row[index_or_header] = value\r
+ end\r
+ end\r
+ end\r
+ end\r
+ end\r
+ \r
+ # \r
+ # The mixed mode default is to treat a list of indices as row access,\r
+ # returning the rows indicated. Anything else is considered columnar\r
+ # access. For columnar access, the return set has an Array for each row\r
+ # with the values indicated by the headers in each Array. You can force\r
+ # column or row mode using by_col!() or by_row!().\r
+ # \r
+ # You cannot mix column and row access.\r
+ # \r
+ def values_at(*indices_or_headers)\r
+ if @mode == :row or # by indices\r
+ ( @mode == :col_or_row and indices_or_headers.all? do |index|\r
+ index.is_a?(Integer) or\r
+ ( index.is_a?(Range) and\r
+ index.first.is_a?(Integer) and\r
+ index.last.is_a?(Integer) )\r
+ end )\r
+ @table.values_at(*indices_or_headers)\r
+ else # by headers\r
+ @table.map { |row| row.values_at(*indices_or_headers) }\r
+ end\r
+ end\r
+\r
+ # \r
+ # Adds a new row to the bottom end of this table. You can provide an Array,\r
+ # which will be converted to a FasterCSV::Row (inheriting the table's\r
+ # headers()), or a FasterCSV::Row.\r
+ # \r
+ # This method returns the table for chaining.\r
+ # \r
+ def <<(row_or_array)\r
+ if row_or_array.is_a? Array # append Array\r
+ @table << Row.new(headers, row_or_array)\r
+ else # append Row\r
+ @table << row_or_array\r
+ end\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # \r
+ # A shortcut for appending multiple rows. Equivalent to:\r
+ # \r
+ # rows.each { |row| self << row }\r
+ # \r
+ # This method returns the table for chaining.\r
+ # \r
+ def push(*rows)\r
+ rows.each { |row| self << row }\r
+ \r
+ self # for chaining\r
+ end\r
+\r
+ # \r
+ # Removes and returns the indicated column or row. In the default mixed\r
+ # mode indices refer to rows and everything else is assumed to be a column\r
+ # header. Use by_col!() or by_row!() to force the lookup.\r
+ # \r
+ def delete(index_or_header)\r
+ if @mode == :row or # by index\r
+ (@mode == :col_or_row and index_or_header.is_a? Integer)\r
+ @table.delete_at(index_or_header)\r
+ else # by header\r
+ @table.map { |row| row.delete(index_or_header).last }\r
+ end\r
+ end\r
+ \r
+ # \r
+ # Removes any column or row for which the block returns +true+. In the\r
+ # default mixed mode or row mode, iteration is the standard row major\r
+ # walking of rows. In column mode, interation will +yield+ two element\r
+ # tuples containing the column name and an Array of values for that column.\r
+ # \r
+ # This method returns the table for chaining.\r
+ # \r
+ def delete_if(&block)\r
+ if @mode == :row or @mode == :col_or_row # by index\r
+ @table.delete_if(&block)\r
+ else # by header\r
+ to_delete = Array.new\r
+ headers.each_with_index do |header, i|\r
+ to_delete << header if block[[header, self[header]]]\r
+ end\r
+ to_delete.map { |header| delete(header) }\r
+ end\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ include Enumerable\r
+ \r
+ # \r
+ # In the default mixed mode or row mode, iteration is the standard row major\r
+ # walking of rows. In column mode, interation will +yield+ two element\r
+ # tuples containing the column name and an Array of values for that column.\r
+ # \r
+ # This method returns the table for chaining.\r
+ # \r
+ def each(&block)\r
+ if @mode == :col\r
+ headers.each { |header| block[[header, self[header]]] }\r
+ else\r
+ @table.each(&block)\r
+ end\r
+ \r
+ self # for chaining\r
+ end\r
+ \r
+ # Returns +true+ if all rows of this table ==() +other+'s rows.\r
+ def ==(other)\r
+ @table == other.table\r
+ end\r
+ \r
+ # \r
+ # Returns the table as an Array of Arrays. Headers will be the first row,\r
+ # then all of the field rows will follow.\r
+ # \r
+ def to_a\r
+ @table.inject([headers]) do |array, row|\r
+ if row.header_row?\r
+ array\r
+ else\r
+ array + [row.fields]\r
+ end\r
+ end\r
+ end\r
+ \r
+ # \r
+ # Returns the table as a complete CSV String. Headers will be listed first,\r
+ # then all of the field rows.\r
+ # \r
+ def to_csv(options = Hash.new)\r
+ @table.inject([headers.to_csv(options)]) do |rows, row|\r
+ if row.header_row?\r
+ rows\r
+ else\r
+ rows + [row.fields.to_csv(options)]\r
+ end\r
+ end.join\r
+ end\r
+ alias_method :to_s, :to_csv\r
+ \r
+ def inspect\r
+ "#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"\r
+ end\r
+ end\r
+\r
+ # The error thrown when the parser encounters illegal CSV formatting.\r
+ class MalformedCSVError < RuntimeError; end\r
+ \r
+ # \r
+ # A FieldInfo Struct contains details about a field's position in the data\r
+ # source it was read from. FasterCSV will pass this Struct to some blocks\r
+ # that make decisions based on field structure. See \r
+ # FasterCSV.convert_fields() for an example.\r
+ # \r
+ # <b><tt>index</tt></b>:: The zero-based index of the field in its row.\r
+ # <b><tt>line</tt></b>:: The line of the data source this row is from.\r
+ # <b><tt>header</tt></b>:: The header for the column, when available.\r
+ # \r
+ FieldInfo = Struct.new(:index, :line, :header)\r
+ \r
+ # A Regexp used to find and convert some common Date formats.\r
+ DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |\r
+ \d{4}-\d{2}-\d{2} )\z /x\r
+ # A Regexp used to find and convert some common DateTime formats.\r
+ DateTimeMatcher =\r
+ / \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |\r
+ \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x\r
+ # \r
+ # This Hash holds the built-in converters of FasterCSV that can be accessed by\r
+ # name. You can select Converters with FasterCSV.convert() or through the\r
+ # +options+ Hash passed to FasterCSV::new().\r
+ # \r
+ # <b><tt>:integer</tt></b>:: Converts any field Integer() accepts.\r
+ # <b><tt>:float</tt></b>:: Converts any field Float() accepts.\r
+ # <b><tt>:numeric</tt></b>:: A combination of <tt>:integer</tt> \r
+ # and <tt>:float</tt>.\r
+ # <b><tt>:date</tt></b>:: Converts any field Date::parse() accepts.\r
+ # <b><tt>:date_time</tt></b>:: Converts any field DateTime::parse() accepts.\r
+ # <b><tt>:all</tt></b>:: All built-in converters. A combination of \r
+ # <tt>:date_time</tt> and <tt>:numeric</tt>.\r
+ # \r
+ # This Hash is intetionally left unfrozen and users should feel free to add\r
+ # values to it that can be accessed by all FasterCSV objects.\r
+ # \r
+ # To add a combo field, the value should be an Array of names. Combo fields\r
+ # can be nested with other combo fields.\r
+ # \r
+ Converters = { :integer => lambda { |f| Integer(f) rescue f },\r
+ :float => lambda { |f| Float(f) rescue f },\r
+ :numeric => [:integer, :float],\r
+ :date => lambda { |f|\r
+ f =~ DateMatcher ? (Date.parse(f) rescue f) : f\r
+ },\r
+ :date_time => lambda { |f|\r
+ f =~ DateTimeMatcher ? (DateTime.parse(f) rescue f) : f\r
+ },\r
+ :all => [:date_time, :numeric] }\r
+\r
+ # \r
+ # This Hash holds the built-in header converters of FasterCSV that can be\r
+ # accessed by name. You can select HeaderConverters with\r
+ # FasterCSV.header_convert() or through the +options+ Hash passed to\r
+ # FasterCSV::new().\r
+ # \r
+ # <b><tt>:downcase</tt></b>:: Calls downcase() on the header String.\r
+ # <b><tt>:symbol</tt></b>:: The header String is downcased, spaces are\r
+ # replaced with underscores, non-word characters\r
+ # are dropped, and finally to_sym() is called.\r
+ # \r
+ # This Hash is intetionally left unfrozen and users should feel free to add\r
+ # values to it that can be accessed by all FasterCSV objects.\r
+ # \r
+ # To add a combo field, the value should be an Array of names. Combo fields\r
+ # can be nested with other combo fields.\r
+ # \r
+ HeaderConverters = {\r
+ :downcase => lambda { |h| h.downcase },\r
+ :symbol => lambda { |h|\r
+ h.downcase.tr(" ", "_").delete("^a-z0-9_").to_sym\r
+ }\r
+ }\r
+ \r
+ # \r
+ # The options used when no overrides are given by calling code. They are:\r
+ # \r
+ # <b><tt>:col_sep</tt></b>:: <tt>","</tt>\r
+ # <b><tt>:row_sep</tt></b>:: <tt>:auto</tt>\r
+ # <b><tt>:quote_char</tt></b>:: <tt>'"'</tt>\r
+ # <b><tt>:converters</tt></b>:: +nil+\r
+ # <b><tt>:unconverted_fields</tt></b>:: +nil+\r
+ # <b><tt>:headers</tt></b>:: +false+\r
+ # <b><tt>:return_headers</tt></b>:: +false+\r
+ # <b><tt>:header_converters</tt></b>:: +nil+\r
+ # <b><tt>:skip_blanks</tt></b>:: +false+\r
+ # <b><tt>:force_quotes</tt></b>:: +false+\r
+ # \r
+ DEFAULT_OPTIONS = { :col_sep => ",",\r
+ :row_sep => :auto,\r
+ :quote_char => '"', \r
+ :converters => nil,\r
+ :unconverted_fields => nil,\r
+ :headers => false,\r
+ :return_headers => false,\r
+ :header_converters => nil,\r
+ :skip_blanks => false,\r
+ :force_quotes => false }.freeze\r
+ \r
+ # \r
+ # This method will build a drop-in replacement for many of the standard CSV\r
+ # methods. It allows you to write code like:\r
+ # \r
+ # begin\r
+ # require "faster_csv"\r
+ # FasterCSV.build_csv_interface\r
+ # rescue LoadError\r
+ # require "csv"\r
+ # end\r
+ # # ... use CSV here ...\r
+ # \r
+ # This is not a complete interface with completely identical behavior.\r
+ # However, it is intended to be close enough that you won't notice the\r
+ # difference in most cases. CSV methods supported are:\r
+ # \r
+ # * foreach()\r
+ # * generate_line()\r
+ # * open()\r
+ # * parse()\r
+ # * parse_line()\r
+ # * readlines()\r
+ # \r
+ # Be warned that this interface is slower than vanilla FasterCSV due to the\r
+ # extra layer of method calls. Depending on usage, this can slow it down to \r
+ # near CSV speeds.\r
+ # \r
+ def self.build_csv_interface\r
+ Object.const_set(:CSV, Class.new).class_eval do\r
+ def self.foreach(path, rs = :auto, &block) # :nodoc:\r
+ FasterCSV.foreach(path, :row_sep => rs, &block)\r
+ end\r
+ \r
+ def self.generate_line(row, fs = ",", rs = "") # :nodoc:\r
+ FasterCSV.generate_line(row, :col_sep => fs, :row_sep => rs)\r
+ end\r
+ \r
+ def self.open(path, mode, fs = ",", rs = :auto, &block) # :nodoc:\r
+ if block and mode.include? "r"\r
+ FasterCSV.open(path, mode, :col_sep => fs, :row_sep => rs) do |csv|\r
+ csv.each(&block)\r
+ end\r
+ else\r
+ FasterCSV.open(path, mode, :col_sep => fs, :row_sep => rs, &block)\r
+ end\r
+ end\r
+ \r
+ def self.parse(str_or_readable, fs = ",", rs = :auto, &block) # :nodoc:\r
+ FasterCSV.parse(str_or_readable, :col_sep => fs, :row_sep => rs, &block)\r
+ end\r
+ \r
+ def self.parse_line(src, fs = ",", rs = :auto) # :nodoc:\r
+ FasterCSV.parse_line(src, :col_sep => fs, :row_sep => rs)\r
+ end\r
+ \r
+ def self.readlines(path, rs = :auto) # :nodoc:\r
+ FasterCSV.readlines(path, :row_sep => rs)\r
+ end\r
+ end\r
+ end\r
+ \r
+ # \r
+ # This method allows you to serialize an Array of Ruby objects to a String or\r
+ # File of CSV data. This is not as powerful as Marshal or YAML, but perhaps\r
+ # useful for spreadsheet and database interaction.\r
+ # \r
+ # Out of the box, this method is intended to work with simple data objects or\r
+ # Structs. It will serialize a list of instance variables and/or\r
+ # Struct.members().\r
+ # \r
+ # If you need need more complicated serialization, you can control the process\r
+ # by adding methods to the class to be serialized.\r
+ # \r
+ # A class method csv_meta() is responsible for returning the first row of the\r
+ # document (as an Array). This row is considered to be a Hash of the form\r
+ # key_1,value_1,key_2,value_2,... FasterCSV::load() expects to find a class\r
+ # key with a value of the stringified class name and FasterCSV::dump() will\r
+ # create this, if you do not define this method. This method is only called\r
+ # on the first object of the Array.\r
+ # \r
+ # The next method you can provide is an instance method called csv_headers().\r
+ # This method is expected to return the second line of the document (again as\r
+ # an Array), which is to be used to give each column a header. By default,\r
+ # FasterCSV::load() will set an instance variable if the field header starts\r
+ # with an @ character or call send() passing the header as the method name and\r
+ # the field value as an argument. This method is only called on the first\r
+ # object of the Array.\r
+ # \r
+ # Finally, you can provide an instance method called csv_dump(), which will\r
+ # be passed the headers. This should return an Array of fields that can be\r
+ # serialized for this object. This method is called once for every object in\r
+ # the Array.\r
+ # \r
+ # The +io+ parameter can be used to serialize to a File, and +options+ can be\r
+ # anything FasterCSV::new() accepts.\r
+ # \r
+ def self.dump(ary_of_objs, io = "", options = Hash.new)\r
+ obj_template = ary_of_objs.first\r
+ \r
+ csv = FasterCSV.new(io, options)\r
+ \r
+ # write meta information\r
+ begin\r
+ csv << obj_template.class.csv_meta\r
+ rescue NoMethodError\r
+ csv << [:class, obj_template.class]\r
+ end\r
+\r
+ # write headers\r
+ begin\r
+ headers = obj_template.csv_headers\r
+ rescue NoMethodError\r
+ headers = obj_template.instance_variables.sort\r
+ if obj_template.class.ancestors.find { |cls| cls.to_s =~ /\AStruct\b/ }\r
+ headers += obj_template.members.map { |mem| "#{mem}=" }.sort\r
+ end\r
+ end\r
+ csv << headers\r
+ \r
+ # serialize each object\r
+ ary_of_objs.each do |obj|\r
+ begin\r
+ csv << obj.csv_dump(headers)\r
+ rescue NoMethodError\r
+ csv << headers.map do |var|\r
+ if var[0] == ?@\r
+ obj.instance_variable_get(var)\r
+ else\r
+ obj[var[0..-2]]\r
+ end\r
+ end\r
+ end\r
+ end\r
+ \r
+ if io.is_a? String\r
+ csv.string\r
+ else\r
+ csv.close\r
+ end\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # filter( options = Hash.new ) { |row| ... }\r
+ # filter( input, options = Hash.new ) { |row| ... }\r
+ # filter( input, output, options = Hash.new ) { |row| ... }\r
+ # \r
+ # This method is a convenience for building Unix-like filters for CSV data.\r
+ # Each row is yielded to the provided block which can alter it as needed. \r
+ # After the block returns, the row is appended to +output+ altered or not.\r
+ # \r
+ # The +input+ and +output+ arguments can be anything FasterCSV::new() accepts\r
+ # (generally String or IO objects). If not given, they default to \r
+ # <tt>ARGF</tt> and <tt>$stdout</tt>.\r
+ # \r
+ # The +options+ parameter is also filtered down to FasterCSV::new() after some\r
+ # clever key parsing. Any key beginning with <tt>:in_</tt> or \r
+ # <tt>:input_</tt> will have that leading identifier stripped and will only\r
+ # be used in the +options+ Hash for the +input+ object. Keys starting with\r
+ # <tt>:out_</tt> or <tt>:output_</tt> affect only +output+. All other keys \r
+ # are assigned to both objects.\r
+ # \r
+ # The <tt>:output_row_sep</tt> +option+ defaults to\r
+ # <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>).\r
+ # \r
+ def self.filter(*args)\r
+ # parse options for input, output, or both\r
+ in_options, out_options = Hash.new, {:row_sep => $INPUT_RECORD_SEPARATOR}\r
+ if args.last.is_a? Hash\r
+ args.pop.each do |key, value|\r
+ case key.to_s\r
+ when /\Ain(?:put)?_(.+)\Z/\r
+ in_options[$1.to_sym] = value\r
+ when /\Aout(?:put)?_(.+)\Z/\r
+ out_options[$1.to_sym] = value\r
+ else\r
+ in_options[key] = value\r
+ out_options[key] = value\r
+ end\r
+ end\r
+ end\r
+ # build input and output wrappers\r
+ input = FasterCSV.new(args.shift || ARGF, in_options)\r
+ output = FasterCSV.new(args.shift || $stdout, out_options)\r
+ \r
+ # read, yield, write\r
+ input.each do |row|\r
+ yield row\r
+ output << row\r
+ end\r
+ end\r
+ \r
+ # \r
+ # This method is intended as the primary interface for reading CSV files. You\r
+ # pass a +path+ and any +options+ you wish to set for the read. Each row of\r
+ # file will be passed to the provided +block+ in turn.\r
+ # \r
+ # The +options+ parameter can be anything FasterCSV::new() understands.\r
+ # \r
+ def self.foreach(path, options = Hash.new, &block)\r
+ open(path, "rb", options) do |csv|\r
+ csv.each(&block)\r
+ end\r
+ end\r
+\r
+ # \r
+ # :call-seq:\r
+ # generate( str, options = Hash.new ) { |faster_csv| ... }\r
+ # generate( options = Hash.new ) { |faster_csv| ... }\r
+ # \r
+ # This method wraps a String you provide, or an empty default String, in a \r
+ # FasterCSV object which is passed to the provided block. You can use the \r
+ # block to append CSV rows to the String and when the block exits, the \r
+ # final String will be returned.\r
+ # \r
+ # Note that a passed String *is* modfied by this method. Call dup() before\r
+ # passing if you need a new String.\r
+ # \r
+ # The +options+ parameter can be anthing FasterCSV::new() understands.\r
+ # \r
+ def self.generate(*args)\r
+ # add a default empty String, if none was given\r
+ if args.first.is_a? String\r
+ io = StringIO.new(args.shift)\r
+ io.seek(0, IO::SEEK_END)\r
+ args.unshift(io)\r
+ else\r
+ args.unshift("")\r
+ end\r
+ faster_csv = new(*args) # wrap\r
+ yield faster_csv # yield for appending\r
+ faster_csv.string # return final String\r
+ end\r
+\r
+ # \r
+ # This method is a shortcut for converting a single row (Array) into a CSV \r
+ # String.\r
+ # \r
+ # The +options+ parameter can be anthing FasterCSV::new() understands.\r
+ # \r
+ # The <tt>:row_sep</tt> +option+ defaults to <tt>$INPUT_RECORD_SEPARATOR</tt>\r
+ # (<tt>$/</tt>) when calling this method.\r
+ # \r
+ def self.generate_line(row, options = Hash.new)\r
+ options = {:row_sep => $INPUT_RECORD_SEPARATOR}.merge(options)\r
+ (new("", options) << row).string\r
+ end\r
+ \r
+ # \r
+ # This method will return a FasterCSV instance, just like FasterCSV::new(), \r
+ # but the instance will be cached and returned for all future calls to this \r
+ # method for the same +data+ object (tested by Object#object_id()) with the\r
+ # same +options+.\r
+ # \r
+ # If a block is given, the instance is passed to the block and the return\r
+ # value becomes the return value of the block.\r
+ # \r
+ def self.instance(data = $stdout, options = Hash.new)\r
+ # create a _signature_ for this method call, data object and options\r
+ sig = [data.object_id] +\r
+ options.values_at(*DEFAULT_OPTIONS.keys.sort_by { |sym| sym.to_s })\r
+ \r
+ # fetch or create the instance for this signature\r
+ @@instances ||= Hash.new\r
+ instance = (@@instances[sig] ||= new(data, options))\r
+\r
+ if block_given?\r
+ yield instance # run block, if given, returning result\r
+ else\r
+ instance # or return the instance\r
+ end\r
+ end\r
+ \r
+ # \r
+ # This method is the reading counterpart to FasterCSV::dump(). See that\r
+ # method for a detailed description of the process.\r
+ # \r
+ # You can customize loading by adding a class method called csv_load() which \r
+ # will be passed a Hash of meta information, an Array of headers, and an Array\r
+ # of fields for the object the method is expected to return.\r
+ # \r
+ # Remember that all fields will be Strings after this load. If you need\r
+ # something else, use +options+ to setup converters or provide a custom\r
+ # csv_load() implementation.\r
+ # \r
+ def self.load(io_or_str, options = Hash.new)\r
+ csv = FasterCSV.new(io_or_str, options)\r
+ \r
+ # load meta information\r
+ meta = Hash[*csv.shift]\r
+ cls = meta["class"].split("::").inject(Object) do |c, const|\r
+ c.const_get(const)\r
+ end\r
+ \r
+ # load headers\r
+ headers = csv.shift\r
+ \r
+ # unserialize each object stored in the file\r
+ results = csv.inject(Array.new) do |all, row|\r
+ begin\r
+ obj = cls.csv_load(meta, headers, row)\r
+ rescue NoMethodError\r
+ obj = cls.allocate\r
+ headers.zip(row) do |name, value|\r
+ if name[0] == ?@\r
+ obj.instance_variable_set(name, value)\r
+ else\r
+ obj.send(name, value)\r
+ end\r
+ end\r
+ end\r
+ all << obj\r
+ end\r
+ \r
+ csv.close unless io_or_str.is_a? String\r
+ \r
+ results\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # open( filename, mode="rb", options = Hash.new ) { |faster_csv| ... }\r
+ # open( filename, mode="rb", options = Hash.new )\r
+ # \r
+ # This method opens an IO object, and wraps that with FasterCSV. This is\r
+ # intended as the primary interface for writing a CSV file.\r
+ # \r
+ # You may pass any +args+ Ruby's open() understands followed by an optional\r
+ # Hash containing any +options+ FasterCSV::new() understands.\r
+ # \r
+ # This method works like Ruby's open() call, in that it will pass a FasterCSV\r
+ # object to a provided block and close it when the block termminates, or it\r
+ # will return the FasterCSV object when no block is provided. (*Note*: This\r
+ # is different from the standard CSV library which passes rows to the block. \r
+ # Use FasterCSV::foreach() for that behavior.)\r
+ # \r
+ # An opened FasterCSV object will delegate to many IO methods, for \r
+ # convenience. You may call:\r
+ # \r
+ # * binmode()\r
+ # * close()\r
+ # * close_read()\r
+ # * close_write()\r
+ # * closed?()\r
+ # * eof()\r
+ # * eof?()\r
+ # * fcntl()\r
+ # * fileno()\r
+ # * flush()\r
+ # * fsync()\r
+ # * ioctl()\r
+ # * isatty()\r
+ # * pid()\r
+ # * pos()\r
+ # * reopen()\r
+ # * seek()\r
+ # * stat()\r
+ # * sync()\r
+ # * sync=()\r
+ # * tell()\r
+ # * to_i()\r
+ # * to_io()\r
+ # * tty?()\r
+ # \r
+ def self.open(*args)\r
+ # find the +options+ Hash\r
+ options = if args.last.is_a? Hash then args.pop else Hash.new end\r
+ # default to a binary open mode\r
+ args << "rb" if args.size == 1\r
+ # wrap a File opened with the remaining +args+\r
+ csv = new(File.open(*args), options)\r
+ \r
+ # handle blocks like Ruby's open(), not like the CSV library\r
+ if block_given?\r
+ begin\r
+ yield csv\r
+ ensure\r
+ csv.close\r
+ end\r
+ else\r
+ csv\r
+ end\r
+ end\r
+ \r
+ # \r
+ # :call-seq:\r
+ # parse( str, options = Hash.new ) { |row| ... }\r
+ # parse( str, options = Hash.new )\r
+ # \r
+ # This method can be used to easily parse CSV out of a String. You may either\r
+ # provide a +block+ which will be called with each row of the String in turn,\r
+ # or just use the returned Array of Arrays (when no +block+ is given).\r
+ # \r
+ # You pass your +str+ to read from, and an optional +options+ Hash containing\r
+ # anything FasterCSV::new() understands.\r
+ # \r
+ def self.parse(*args, &block)\r
+ csv = new(*args)\r
+ if block.nil? # slurp contents, if no block is given\r
+ begin\r
+ csv.read\r
+ ensure\r
+ csv.close\r
+ end\r
+ else # or pass each row to a provided block\r
+ csv.each(&block)\r
+ end\r
+ end\r
+ \r
+ # \r
+ # This method is a shortcut for converting a single line of a CSV String into \r
+ # a into an Array. Note that if +line+ contains multiple rows, anything \r
+ # beyond the first row is ignored.\r
+ # \r
+ # The +options+ parameter can be anthing FasterCSV::new() understands.\r
+ # \r
+ def self.parse_line(line, options = Hash.new)\r
+ new(line, options).shift\r
+ end\r
+ \r
+ # \r
+ # Use to slurp a CSV file into an Array of Arrays. Pass the +path+ to the \r
+ # file and any +options+ FasterCSV::new() understands.\r
+ # \r
+ def self.read(path, options = Hash.new)\r
+ open(path, "rb", options) { |csv| csv.read }\r
+ end\r
+ \r
+ # Alias for FasterCSV::read().\r
+ def self.readlines(*args)\r
+ read(*args)\r
+ end\r
+ \r
+ # \r
+ # A shortcut for:\r
+ # \r
+ # FasterCSV.read( path, { :headers => true,\r
+ # :converters => :numeric,\r
+ # :header_converters => :symbol }.merge(options) )\r
+ # \r
+ def self.table(path, options = Hash.new)\r
+ read( path, { :headers => true,\r
+ :converters => :numeric,\r
+ :header_converters => :symbol }.merge(options) )\r
+ end\r
+ \r
+ # \r
+ # This constructor will wrap either a String or IO object passed in +data+ for\r
+ # reading and/or writing. In addition to the FasterCSV instance methods, \r
+ # several IO methods are delegated. (See FasterCSV::open() for a complete \r
+ # list.) If you pass a String for +data+, you can later retrieve it (after\r
+ # writing to it, for example) with FasterCSV.string().\r
+ # \r
+ # Note that a wrapped String will be positioned at at the beginning (for \r
+ # reading). If you want it at the end (for writing), use \r
+ # FasterCSV::generate(). If you want any other positioning, pass a preset \r
+ # StringIO object instead.\r
+ # \r
+ # You may set any reading and/or writing preferences in the +options+ Hash. \r
+ # Available options are:\r
+ # \r
+ # <b><tt>:col_sep</tt></b>:: The String placed between each field.\r
+ # <b><tt>:row_sep</tt></b>:: The String appended to the end of each\r
+ # row. This can be set to the special\r
+ # <tt>:auto</tt> setting, which requests\r
+ # that FasterCSV automatically discover\r
+ # this from the data. Auto-discovery\r
+ # reads ahead in the data looking for\r
+ # the next <tt>"\r\n"</tt>,\r
+ # <tt>"\n"</tt>, or <tt>"\r"</tt>\r
+ # sequence. A sequence will be selected\r
+ # even if it occurs in a quoted field,\r
+ # assuming that you would have the same\r
+ # line endings there. If none of those\r
+ # sequences is found, +data+ is\r
+ # <tt>ARGF</tt>, <tt>STDIN</tt>,\r
+ # <tt>STDOUT</tt>, or <tt>STDERR</tt>,\r
+ # or the stream is only available for\r
+ # output, the default\r
+ # <tt>$INPUT_RECORD_SEPARATOR</tt>\r
+ # (<tt>$/</tt>) is used. Obviously,\r
+ # discovery takes a little time. Set\r
+ # manually if speed is important. Also\r
+ # note that IO objects should be opened\r
+ # in binary mode on Windows if this\r
+ # feature will be used as the\r
+ # line-ending translation can cause\r
+ # problems with resetting the document\r
+ # position to where it was before the\r
+ # read ahead.\r
+ # <b><tt>:quote_char</tt></b>:: The character used to quote fields.\r
+ # This has to be a single character\r
+ # String. This is useful for\r
+ # application that incorrectly use\r
+ # <tt>'</tt> as the quote character\r
+ # instead of the correct <tt>"</tt>.\r
+ # FasterCSV will always consider a\r
+ # double sequence this character to be\r
+ # an escaped quote.\r
+ # <b><tt>:encoding</tt></b>:: The encoding to use when parsing the\r
+ # file. Defaults to your <tt>$KDOCE</tt>\r
+ # setting. Valid values: <tt>`n’</tt> or\r
+ # <tt>`N’</tt> for none, <tt>`e’</tt> or\r
+ # <tt>`E’</tt> for EUC, <tt>`s’</tt> or\r
+ # <tt>`S’</tt> for SJIS, and\r
+ # <tt>`u’</tt> or <tt>`U’</tt> for UTF-8\r
+ # (see Regexp.new()).\r
+ # <b><tt>:field_size_limit</tt></b>:: This is a maximum size FasterCSV will\r
+ # read ahead looking for the closing\r
+ # quote for a field. (In truth, it\r
+ # reads to the first line ending beyond\r
+ # this size.) If a quote cannot be\r
+ # found within the limit FasterCSV will\r
+ # raise a MalformedCSVError, assuming\r
+ # the data is faulty. You can use this\r
+ # limit to prevent what are effectively\r
+ # DoS attacks on the parser. However,\r
+ # this limit can cause a legitimate\r
+ # parse to fail and thus is set to\r
+ # +nil+, or off, by default.\r
+ # <b><tt>:converters</tt></b>:: An Array of names from the Converters\r
+ # Hash and/or lambdas that handle custom\r
+ # conversion. A single converter\r
+ # doesn't have to be in an Array.\r
+ # <b><tt>:unconverted_fields</tt></b>:: If set to +true+, an\r
+ # unconverted_fields() method will be\r
+ # added to all returned rows (Array or\r
+ # FasterCSV::Row) that will return the\r
+ # fields as they were before convertion.\r
+ # Note that <tt>:headers</tt> supplied\r
+ # by Array or String were not fields of\r
+ # the document and thus will have an\r
+ # empty Array attached.\r
+ # <b><tt>:headers</tt></b>:: If set to <tt>:first_row</tt> or \r
+ # +true+, the initial row of the CSV\r
+ # file will be treated as a row of\r
+ # headers. If set to an Array, the\r
+ # contents will be used as the headers.\r
+ # If set to a String, the String is run\r
+ # through a call of\r
+ # FasterCSV::parse_line() with the same\r
+ # <tt>:col_sep</tt>, <tt>:row_sep</tt>,\r
+ # and <tt>:quote_char</tt> as this\r
+ # instance to produce an Array of\r
+ # headers. This setting causes\r
+ # FasterCSV.shift() to return rows as\r
+ # FasterCSV::Row objects instead of\r
+ # Arrays and FasterCSV.read() to return\r
+ # FasterCSV::Table objects instead of\r
+ # an Array of Arrays.\r
+ # <b><tt>:return_headers</tt></b>:: When +false+, header rows are silently\r
+ # swallowed. If set to +true+, header\r
+ # rows are returned in a FasterCSV::Row\r
+ # object with identical headers and\r
+ # fields (save that the fields do not go\r
+ # through the converters).\r
+ # <b><tt>:write_headers</tt></b>:: When +true+ and <tt>:headers</tt> is\r
+ # set, a header row will be added to the\r
+ # output.\r
+ # <b><tt>:header_converters</tt></b>:: Identical in functionality to\r
+ # <tt>:converters</tt> save that the\r
+ # conversions are only made to header\r
+ # rows.\r
+ # <b><tt>:skip_blanks</tt></b>:: When set to a +true+ value, FasterCSV\r
+ # will skip over any rows with no\r
+ # content.\r
+ # <b><tt>:force_quotes</tt></b>:: When set to a +true+ value, FasterCSV\r
+ # will quote all CSV fields it creates.\r
+ # \r
+ # See FasterCSV::DEFAULT_OPTIONS for the default settings.\r
+ # \r
+ # Options cannot be overriden in the instance methods for performance reasons,\r
+ # so be sure to set what you want here.\r
+ # \r
+ def initialize(data, options = Hash.new)\r
+ # build the options for this read/write\r
+ options = DEFAULT_OPTIONS.merge(options)\r
+ \r
+ # create the IO object we will read from\r
+ @io = if data.is_a? String then StringIO.new(data) else data end\r
+ \r
+ init_separators(options)\r
+ init_parsers(options)\r
+ init_converters(options)\r
+ init_headers(options)\r
+ \r
+ unless options.empty?\r
+ raise ArgumentError, "Unknown options: #{options.keys.join(', ')}."\r
+ end\r
+ \r
+ # track our own lineno since IO gets confused about line-ends is CSV fields\r
+ @lineno = 0\r
+ end\r
+ \r
+ # \r
+ # The line number of the last row read from this file. Fields with nested \r
+ # line-end characters will not affect this count.\r
+ # \r
+ attr_reader :lineno\r
+ \r
+ ### IO and StringIO Delegation ###\r
+ \r
+ extend Forwardable\r
+ def_delegators :@io, :binmode, :close, :close_read, :close_write, :closed?,\r
+ :eof, :eof?, :fcntl, :fileno, :flush, :fsync, :ioctl,\r
+ :isatty, :pid, :pos, :reopen, :seek, :stat, :string,\r
+ :sync, :sync=, :tell, :to_i, :to_io, :tty?\r
+ \r
+ # Rewinds the underlying IO object and resets FasterCSV's lineno() counter.\r
+ def rewind\r
+ @headers = nil\r
+ @lineno = 0\r
+ \r
+ @io.rewind\r
+ end\r
+\r
+ ### End Delegation ###\r
+ \r
+ # \r
+ # The primary write method for wrapped Strings and IOs, +row+ (an Array or\r
+ # FasterCSV::Row) is converted to CSV and appended to the data source. When a\r
+ # FasterCSV::Row is passed, only the row's fields() are appended to the\r
+ # output.\r
+ # \r
+ # The data source must be open for writing.\r
+ # \r
+ def <<(row)\r
+ # make sure headers have been assigned\r
+ if header_row? and [Array, String].include? @use_headers.class\r
+ parse_headers # won't read data for Array or String\r
+ self << @headers if @write_headers\r
+ end\r
+ \r
+ # Handle FasterCSV::Row objects and Hashes\r
+ row = case row\r
+ when self.class::Row then row.fields\r
+ when Hash then @headers.map { |header| row[header] }\r
+ else row\r
+ end\r
+\r
+ @headers = row if header_row?\r
+ @lineno += 1\r
+\r
+ @io << row.map(&@quote).join(@col_sep) + @row_sep # quote and separate\r
+ \r
+ self # for chaining\r
+ end\r
+ alias_method :add_row, :<<\r
+ alias_method :puts, :<<\r
+ \r
+ # \r
+ # :call-seq:\r
+ # convert( name )\r
+ # convert { |field| ... }\r
+ # convert { |field, field_info| ... }\r
+ # \r
+ # You can use this method to install a FasterCSV::Converters built-in, or \r
+ # provide a block that handles a custom conversion.\r
+ # \r
+ # If you provide a block that takes one argument, it will be passed the field\r
+ # and is expected to return the converted value or the field itself. If your\r
+ # block takes two arguments, it will also be passed a FieldInfo Struct, \r
+ # containing details about the field. Again, the block should return a \r
+ # converted field or the field itself.\r
+ # \r
+ def convert(name = nil, &converter)\r
+ add_converter(:converters, self.class::Converters, name, &converter)\r
+ end\r
+\r
+ # \r
+ # :call-seq:\r
+ # header_convert( name )\r
+ # header_convert { |field| ... }\r
+ # header_convert { |field, field_info| ... }\r
+ # \r
+ # Identical to FasterCSV.convert(), but for header rows.\r
+ # \r
+ # Note that this method must be called before header rows are read to have any\r
+ # effect.\r
+ # \r
+ def header_convert(name = nil, &converter)\r
+ add_converter( :header_converters,\r
+ self.class::HeaderConverters,\r
+ name,\r
+ &converter )\r
+ end\r
+ \r
+ include Enumerable\r
+ \r
+ # \r
+ # Yields each row of the data source in turn.\r
+ # \r
+ # Support for Enumerable.\r
+ # \r
+ # The data source must be open for reading.\r
+ # \r
+ def each\r
+ while row = shift\r
+ yield row\r
+ end\r
+ end\r
+ \r
+ # \r
+ # Slurps the remaining rows and returns an Array of Arrays.\r
+ # \r
+ # The data source must be open for reading.\r
+ # \r
+ def read\r
+ rows = to_a\r
+ if @use_headers\r
+ Table.new(rows)\r
+ else\r
+ rows\r
+ end\r
+ end\r
+ alias_method :readlines, :read\r
+ \r
+ # Returns +true+ if the next row read will be a header row.\r
+ def header_row?\r
+ @use_headers and @headers.nil?\r
+ end\r
+ \r
+ # \r
+ # The primary read method for wrapped Strings and IOs, a single row is pulled\r
+ # from the data source, parsed and returned as an Array of fields (if header\r
+ # rows are not used) or a FasterCSV::Row (when header rows are used).\r
+ # \r
+ # The data source must be open for reading.\r
+ # \r
+ def shift\r
+ #########################################################################\r
+ ### This method is purposefully kept a bit long as simple conditional ###\r
+ ### checks are faster than numerous (expensive) method calls. ###\r
+ #########################################################################\r
+ \r
+ # handle headers not based on document content\r
+ if header_row? and @return_headers and\r
+ [Array, String].include? @use_headers.class\r
+ if @unconverted_fields\r
+ return add_unconverted_fields(parse_headers, Array.new)\r
+ else\r
+ return parse_headers\r
+ end\r
+ end\r
+ \r
+ # begin with a blank line, so we can always add to it\r
+ line = String.new\r
+\r
+ # \r
+ # it can take multiple calls to <tt>@io.gets()</tt> to get a full line,\r
+ # because of \r and/or \n characters embedded in quoted fields\r
+ # \r
+ loop do\r
+ # add another read to the line\r
+ begin\r
+ line += @io.gets(@row_sep)\r
+ rescue\r
+ return nil\r
+ end\r
+ # copy the line so we can chop it up in parsing\r
+ parse = line.dup\r
+ parse.sub!(@parsers[:line_end], "")\r
+ \r
+ # \r
+ # I believe a blank line should be an <tt>Array.new</tt>, not \r
+ # CSV's <tt>[nil]</tt>\r
+ # \r
+ if parse.empty?\r
+ @lineno += 1\r
+ if @skip_blanks\r
+ line = ""\r
+ next\r
+ elsif @unconverted_fields\r
+ return add_unconverted_fields(Array.new, Array.new)\r
+ elsif @use_headers\r
+ return FasterCSV::Row.new(Array.new, Array.new)\r
+ else\r
+ return Array.new\r
+ end\r
+ end\r
+\r
+ # parse the fields with a mix of String#split and regular expressions\r
+ csv = Array.new\r
+ current_field = String.new\r
+ field_quotes = 0\r
+ parse.split(@col_sep, -1).each do |match|\r
+ if current_field.empty? && match.count(@quote_and_newlines).zero?\r
+ csv << (match.empty? ? nil : match)\r
+ elsif(current_field.empty? ? match[0] : current_field[0]) == @quote_char[0]\r
+ current_field << match\r
+ field_quotes += match.count(@quote_char)\r
+ if field_quotes % 2 == 0\r
+ in_quotes = current_field[@parsers[:quoted_field], 1]\r
+ raise MalformedCSVError unless in_quotes\r
+ current_field = in_quotes\r
+ current_field.gsub!(@quote_char * 2, @quote_char) # unescape contents\r
+ csv << current_field\r
+ current_field = String.new\r
+ field_quotes = 0\r
+ else # we found a quoted field that spans multiple lines\r
+ current_field << @col_sep\r
+ end\r
+ elsif match.count("\r\n").zero?\r
+ raise MalformedCSVError, "Illegal quoting on line #{lineno + 1}."\r
+ else\r
+ raise MalformedCSVError, "Unquoted fields do not allow " +\r
+ "\\r or \\n (line #{lineno + 1})."\r
+ end\r
+ end\r
+\r
+ # if parse is empty?(), we found all the fields on the line...\r
+ if field_quotes % 2 == 0\r
+ @lineno += 1\r
+\r
+ # save fields unconverted fields, if needed...\r
+ unconverted = csv.dup if @unconverted_fields\r
+\r
+ # convert fields, if needed...\r
+ csv = convert_fields(csv) unless @use_headers or @converters.empty?\r
+ # parse out header rows and handle FasterCSV::Row conversions...\r
+ csv = parse_headers(csv) if @use_headers\r
+\r
+ # inject unconverted fields and accessor, if requested...\r
+ if @unconverted_fields and not csv.respond_to? :unconverted_fields\r
+ add_unconverted_fields(csv, unconverted)\r
+ end\r
+\r
+ # return the results\r
+ break csv\r
+ end\r
+ # if we're not empty?() but at eof?(), a quoted field wasn't closed...\r
+ if @io.eof?\r
+ raise MalformedCSVError, "Unclosed quoted field on line #{lineno + 1}."\r
+ elsif @field_size_limit and current_field.size >= @field_size_limit\r
+ raise MalformedCSVError, "Field size exceeded on line #{lineno + 1}."\r
+ end\r
+ # otherwise, we need to loop and pull some more data to complete the row\r
+ end\r
+ end\r
+ alias_method :gets, :shift\r
+ alias_method :readline, :shift\r
+ \r
+ # Returns a simplified description of the key FasterCSV attributes.\r
+ def inspect\r
+ str = "<##{self.class} io_type:"\r
+ # show type of wrapped IO\r
+ if @io == $stdout then str << "$stdout"\r
+ elsif @io == $stdin then str << "$stdin"\r
+ elsif @io == $stderr then str << "$stderr"\r
+ else str << @io.class.to_s\r
+ end\r
+ # show IO.path(), if available\r
+ if @io.respond_to?(:path) and (p = @io.path)\r
+ str << " io_path:#{p.inspect}"\r
+ end\r
+ # show other attributes\r
+ %w[ lineno col_sep row_sep\r
+ quote_char skip_blanks encoding ].each do |attr_name|\r
+ if a = instance_variable_get("@#{attr_name}")\r
+ str << " #{attr_name}:#{a.inspect}"\r
+ end\r
+ end\r
+ if @use_headers\r
+ str << " headers:#{(@headers || true).inspect}"\r
+ end\r
+ str << ">"\r
+ end\r
+ \r
+ private\r
+ \r
+ # \r
+ # Stores the indicated separators for later use.\r
+ # \r
+ # If auto-discovery was requested for <tt>@row_sep</tt>, this method will read\r
+ # ahead in the <tt>@io</tt> and try to find one. +ARGF+, +STDIN+, +STDOUT+,\r
+ # +STDERR+ and any stream open for output only with a default\r
+ # <tt>@row_sep</tt> of <tt>$INPUT_RECORD_SEPARATOR</tt> (<tt>$/</tt>).\r
+ # \r
+ # This method also establishes the quoting rules used for CSV output.\r
+ # \r
+ def init_separators(options)\r
+ # store the selected separators\r
+ @col_sep = options.delete(:col_sep)\r
+ @row_sep = options.delete(:row_sep)\r
+ @quote_char = options.delete(:quote_char)\r
+ @quote_and_newlines = "#{@quote_char}\r\n"\r
+\r
+ if @quote_char.length != 1\r
+ raise ArgumentError, ":quote_char has to be a single character String"\r
+ end\r
+ \r
+ # automatically discover row separator when requested\r
+ if @row_sep == :auto\r
+ if [ARGF, STDIN, STDOUT, STDERR].include?(@io) or\r
+ (defined?(Zlib) and @io.class == Zlib::GzipWriter)\r
+ @row_sep = $INPUT_RECORD_SEPARATOR\r
+ else\r
+ begin\r
+ saved_pos = @io.pos # remember where we were\r
+ while @row_sep == :auto\r
+ # \r
+ # if we run out of data, it's probably a single line \r
+ # (use a sensible default)\r
+ # \r
+ if @io.eof?\r
+ @row_sep = $INPUT_RECORD_SEPARATOR\r
+ break\r
+ end\r
+ \r
+ # read ahead a bit\r
+ sample = @io.read(1024)\r
+ sample += @io.read(1) if sample[-1..-1] == "\r" and not @io.eof?\r
+ \r
+ # try to find a standard separator\r
+ if sample =~ /\r\n?|\n/\r
+ @row_sep = $&\r
+ break\r
+ end\r
+ end\r
+ # tricky seek() clone to work around GzipReader's lack of seek()\r
+ @io.rewind\r
+ # reset back to the remembered position\r
+ while saved_pos > 1024 # avoid loading a lot of data into memory\r
+ @io.read(1024)\r
+ saved_pos -= 1024\r
+ end\r
+ @io.read(saved_pos) if saved_pos.nonzero?\r
+ rescue IOError # stream not opened for reading\r
+ @row_sep = $INPUT_RECORD_SEPARATOR\r
+ end\r
+ end\r
+ end\r
+ \r
+ # establish quoting rules\r
+ do_quote = lambda do |field|\r
+ @quote_char +\r
+ String(field).gsub(@quote_char, @quote_char * 2) +\r
+ @quote_char\r
+ end\r
+ @quote = if options.delete(:force_quotes)\r
+ do_quote\r
+ else\r
+ lambda do |field|\r
+ if field.nil? # represent +nil+ fields as empty unquoted fields\r
+ ""\r
+ else\r
+ field = String(field) # Stringify fields\r
+ # represent empty fields as empty quoted fields\r
+ if field.empty? or\r
+ field.count("\r\n#{@col_sep}#{@quote_char}").nonzero?\r
+ do_quote.call(field)\r
+ else\r
+ field # unquoted field\r
+ end\r
+ end\r
+ end\r
+ end\r
+ end\r
+ \r
+ # Pre-compiles parsers and stores them by name for access during reads.\r
+ def init_parsers(options)\r
+ # store the parser behaviors\r
+ @skip_blanks = options.delete(:skip_blanks)\r
+ @encoding = options.delete(:encoding) # nil will use $KCODE\r
+ @field_size_limit = options.delete(:field_size_limit)\r
+\r
+ # prebuild Regexps for faster parsing\r
+ esc_col_sep = Regexp.escape(@col_sep)\r
+ esc_row_sep = Regexp.escape(@row_sep)\r
+ esc_quote = Regexp.escape(@quote_char)\r
+ @parsers = {\r
+ :any_field => Regexp.new( "[^#{esc_col_sep}]+",\r
+ Regexp::MULTILINE,\r
+ @encoding ),\r
+ :quoted_field => Regexp.new( "^#{esc_quote}(.*)#{esc_quote}$",\r
+ Regexp::MULTILINE,\r
+ @encoding ),\r
+ # safer than chomp!()\r
+ :line_end => Regexp.new("#{esc_row_sep}\\z", nil, @encoding)\r
+ }\r
+ end\r
+ \r
+ # \r
+ # Loads any converters requested during construction.\r
+ # \r
+ # If +field_name+ is set <tt>:converters</tt> (the default) field converters\r
+ # are set. When +field_name+ is <tt>:header_converters</tt> header converters\r
+ # are added instead.\r
+ # \r
+ # The <tt>:unconverted_fields</tt> option is also actived for \r
+ # <tt>:converters</tt> calls, if requested.\r
+ # \r
+ def init_converters(options, field_name = :converters)\r
+ if field_name == :converters\r
+ @unconverted_fields = options.delete(:unconverted_fields)\r
+ end\r
+\r
+ instance_variable_set("@#{field_name}", Array.new)\r
+ \r
+ # find the correct method to add the coverters\r
+ convert = method(field_name.to_s.sub(/ers\Z/, ""))\r
+ \r
+ # load converters\r
+ unless options[field_name].nil?\r
+ # allow a single converter not wrapped in an Array\r
+ unless options[field_name].is_a? Array\r
+ options[field_name] = [options[field_name]]\r
+ end\r
+ # load each converter...\r
+ options[field_name].each do |converter|\r
+ if converter.is_a? Proc # custom code block\r
+ convert.call(&converter)\r
+ else # by name\r
+ convert.call(converter)\r
+ end\r
+ end\r
+ end\r
+ \r
+ options.delete(field_name)\r
+ end\r
+ \r
+ # Stores header row settings and loads header converters, if needed.\r
+ def init_headers(options)\r
+ @use_headers = options.delete(:headers)\r
+ @return_headers = options.delete(:return_headers)\r
+ @write_headers = options.delete(:write_headers)\r
+\r
+ # headers must be delayed until shift(), in case they need a row of content\r
+ @headers = nil\r
+ \r
+ init_converters(options, :header_converters)\r
+ end\r
+ \r
+ # \r
+ # The actual work method for adding converters, used by both \r
+ # FasterCSV.convert() and FasterCSV.header_convert().\r
+ # \r
+ # This method requires the +var_name+ of the instance variable to place the\r
+ # converters in, the +const+ Hash to lookup named converters in, and the\r
+ # normal parameters of the FasterCSV.convert() and FasterCSV.header_convert()\r
+ # methods.\r
+ # \r
+ def add_converter(var_name, const, name = nil, &converter)\r
+ if name.nil? # custom converter\r
+ instance_variable_get("@#{var_name}") << converter\r
+ else # named converter\r
+ combo = const[name]\r
+ case combo\r
+ when Array # combo converter\r
+ combo.each do |converter_name|\r
+ add_converter(var_name, const, converter_name)\r
+ end\r
+ else # individual named converter\r
+ instance_variable_get("@#{var_name}") << combo\r
+ end\r
+ end\r
+ end\r
+ \r
+ # \r
+ # Processes +fields+ with <tt>@converters</tt>, or <tt>@header_converters</tt>\r
+ # if +headers+ is passed as +true+, returning the converted field set. Any\r
+ # converter that changes the field into something other than a String halts\r
+ # the pipeline of conversion for that field. This is primarily an efficiency\r
+ # shortcut.\r
+ # \r
+ def convert_fields(fields, headers = false)\r
+ # see if we are converting headers or fields\r
+ converters = headers ? @header_converters : @converters\r
+ \r
+ fields.enum_for(:each_with_index).map do |field, index| # map_with_index\r
+ converters.each do |converter|\r
+ field = if converter.arity == 1 # straight field converter\r
+ converter[field]\r
+ else # FieldInfo converter\r
+ header = @use_headers && !headers ? @headers[index] : nil\r
+ converter[field, FieldInfo.new(index, lineno, header)]\r
+ end\r
+ break unless field.is_a? String # short-curcuit pipeline for speed\r
+ end\r
+ field # return final state of each field, converted or original\r
+ end\r
+ end\r
+ \r
+ # \r
+ # This methods is used to turn a finished +row+ into a FasterCSV::Row. Header\r
+ # rows are also dealt with here, either by returning a FasterCSV::Row with\r
+ # identical headers and fields (save that the fields do not go through the\r
+ # converters) or by reading past them to return a field row. Headers are also\r
+ # saved in <tt>@headers</tt> for use in future rows.\r
+ # \r
+ # When +nil+, +row+ is assumed to be a header row not based on an actual row\r
+ # of the stream.\r
+ # \r
+ def parse_headers(row = nil)\r
+ if @headers.nil? # header row\r
+ @headers = case @use_headers # save headers\r
+ # Array of headers\r
+ when Array then @use_headers\r
+ # CSV header String\r
+ when String\r
+ self.class.parse_line( @use_headers,\r
+ :col_sep => @col_sep,\r
+ :row_sep => @row_sep,\r
+ :quote_char => @quote_char )\r
+ # first row is headers\r
+ else row\r
+ end\r
+ \r
+ # prepare converted and unconverted copies\r
+ row = @headers if row.nil?\r
+ @headers = convert_fields(@headers, true)\r
+ \r
+ if @return_headers # return headers\r
+ return FasterCSV::Row.new(@headers, row, true)\r
+ elsif not [Array, String].include? @use_headers.class # skip to field row\r
+ return shift\r
+ end\r
+ end\r
+\r
+ FasterCSV::Row.new(@headers, convert_fields(row)) # field row\r
+ end\r
+ \r
+ # \r
+ # Thiw methods injects an instance variable <tt>unconverted_fields</tt> into\r
+ # +row+ and an accessor method for it called unconverted_fields(). The\r
+ # variable is set to the contents of +fields+.\r
+ # \r
+ def add_unconverted_fields(row, fields)\r
+ class << row\r
+ attr_reader :unconverted_fields\r
+ end\r
+ row.instance_eval { @unconverted_fields = fields }\r
+ row\r
+ end\r
+end\r
+\r
+# Another name for FasterCSV.\r
+FCSV = FasterCSV\r
+\r
+# Another name for FasterCSV::instance().\r
+def FasterCSV(*args, &block)\r
+ FasterCSV.instance(*args, &block)\r
+end\r
+\r
+# Another name for FCSV::instance().\r
+def FCSV(*args, &block)\r
+ FCSV.instance(*args, &block)\r
+end\r
+\r
+class Array\r
+ # Equivalent to <tt>FasterCSV::generate_line(self, options)</tt>.\r
+ def to_csv(options = Hash.new)\r
+ FasterCSV.generate_line(self, options)\r
+ end\r
+end\r
+\r
+class String\r
+ # Equivalent to <tt>FasterCSV::parse_line(self, options)</tt>.\r
+ def parse_csv(options = Hash.new)\r
+ FasterCSV.parse_line(self, options)\r
+ end\r
+end\r
--- /dev/null
+Description:
+ The plugin generator creates stubs for a new Redmine plugin.
+
+Example:
+ ./script/generate redmine_plugin meetings
+ create vendor/plugins/redmine_meetings/app/controllers
+ create vendor/plugins/redmine_meetings/app/helpers
+ create vendor/plugins/redmine_meetings/app/models
+ create vendor/plugins/redmine_meetings/app/views
+ create vendor/plugins/redmine_meetings/db/migrate
+ create vendor/plugins/redmine_meetings/lib/tasks
+ create vendor/plugins/redmine_meetings/assets/images
+ create vendor/plugins/redmine_meetings/assets/javascripts
+ create vendor/plugins/redmine_meetings/assets/stylesheets
+ create vendor/plugins/redmine_meetings/lang
+ create vendor/plugins/redmine_meetings/README
+ create vendor/plugins/redmine_meetings/init.rb
+ create vendor/plugins/redmine_meetings/lang/en.yml
+ create vendor/plugins/redmine_meetings/config/locales/en.yml
+ create vendor/plugins/redmine_meetings/test/test_helper.rb
--- /dev/null
+class RedminePluginGenerator < Rails::Generator::NamedBase
+ attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
+
+ def initialize(runtime_args, runtime_options = {})
+ super
+ @plugin_name = "redmine_#{file_name.underscore}"
+ @plugin_pretty_name = plugin_name.titleize
+ @plugin_path = "vendor/plugins/#{plugin_name}"
+ end
+
+ def manifest
+ record do |m|
+ m.directory "#{plugin_path}/app/controllers"
+ m.directory "#{plugin_path}/app/helpers"
+ m.directory "#{plugin_path}/app/models"
+ m.directory "#{plugin_path}/app/views"
+ m.directory "#{plugin_path}/db/migrate"
+ m.directory "#{plugin_path}/lib/tasks"
+ m.directory "#{plugin_path}/assets/images"
+ m.directory "#{plugin_path}/assets/javascripts"
+ m.directory "#{plugin_path}/assets/stylesheets"
+ m.directory "#{plugin_path}/lang"
+ m.directory "#{plugin_path}/config/locales"
+ m.directory "#{plugin_path}/test"
+
+ m.template 'README.rdoc', "#{plugin_path}/README.rdoc"
+ m.template 'init.rb.erb', "#{plugin_path}/init.rb"
+ m.template 'en.yml', "#{plugin_path}/lang/en.yml"
+ m.template 'en_rails_i18n.yml', "#{plugin_path}/config/locales/en.yml"
+ m.template 'test_helper.rb.erb', "#{plugin_path}/test/test_helper.rb"
+ end
+ end
+end
--- /dev/null
+= <%= file_name %>
+
+Description goes here
--- /dev/null
+# English strings go here
+my_label: "My label"
--- /dev/null
+# English strings go here for Rails i18n
+en:
+ my_label: "My label"
--- /dev/null
+require 'redmine'
+
+Redmine::Plugin.register :<%= plugin_name %> do
+ name '<%= plugin_pretty_name %> plugin'
+ author 'Author name'
+ description 'This is a plugin for Redmine'
+ version '0.0.1'
+end
--- /dev/null
+# Load the normal Rails helper
+require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
+
+# Ensure that we are using the temporary fixture path
+Engines::Testing.set_fixture_path
--- /dev/null
+Description:
+ Generates a plugin controller.
+
+Example:
+ ./script/generate redmine_plugin_controller MyPlugin Pools index show vote
--- /dev/null
+require 'rails_generator/base'
+require 'rails_generator/generators/components/controller/controller_generator'
+
+class RedminePluginControllerGenerator < ControllerGenerator
+ attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
+
+ def initialize(runtime_args, runtime_options = {})
+ runtime_args = runtime_args.dup
+ @plugin_name = "redmine_" + runtime_args.shift.underscore
+ @plugin_pretty_name = plugin_name.titleize
+ @plugin_path = "vendor/plugins/#{plugin_name}"
+ super(runtime_args, runtime_options)
+ end
+
+ def destination_root
+ File.join(RAILS_ROOT, plugin_path)
+ end
+
+ def manifest
+ record do |m|
+ # Check for class naming collisions.
+ m.class_collisions class_path, "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper"
+
+ # Controller, helper, views, and test directories.
+ m.directory File.join('app/controllers', class_path)
+ m.directory File.join('app/helpers', class_path)
+ m.directory File.join('app/views', class_path, file_name)
+ m.directory File.join('test/functional', class_path)
+
+ # Controller class, functional test, and helper class.
+ m.template 'controller.rb.erb',
+ File.join('app/controllers',
+ class_path,
+ "#{file_name}_controller.rb")
+
+ m.template 'functional_test.rb.erb',
+ File.join('test/functional',
+ class_path,
+ "#{file_name}_controller_test.rb")
+
+ m.template 'helper.rb.erb',
+ File.join('app/helpers',
+ class_path,
+ "#{file_name}_helper.rb")
+
+ # View template for each action.
+ actions.each do |action|
+ path = File.join('app/views', class_path, file_name, "#{action}.html.erb")
+ m.template 'view.html.erb', path,
+ :assigns => { :action => action, :path => path }
+ end
+ end
+ end
+end
--- /dev/null
+class <%= class_name %>Controller < ApplicationController
+<% actions.each do |action| -%>
+
+ def <%= action %>
+ end
+<% end -%>
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class <%= class_name %>ControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
--- /dev/null
+module <%= class_name %>Helper
+end
--- /dev/null
+<h2><%= class_name %>#<%= action %></h2>
--- /dev/null
+Description:
+ Generates a plugin model.
+
+Examples:
+ ./script/generate redmine_plugin_model MyPlugin pool title:string question:text
--- /dev/null
+require 'rails_generator/base'
+require 'rails_generator/generators/components/model/model_generator'
+
+class RedminePluginModelGenerator < ModelGenerator
+ attr_accessor :plugin_path, :plugin_name, :plugin_pretty_name
+
+ def initialize(runtime_args, runtime_options = {})
+ runtime_args = runtime_args.dup
+ @plugin_name = "redmine_" + runtime_args.shift.underscore
+ @plugin_pretty_name = plugin_name.titleize
+ @plugin_path = "vendor/plugins/#{plugin_name}"
+ super(runtime_args, runtime_options)
+ end
+
+ def destination_root
+ File.join(RAILS_ROOT, plugin_path)
+ end
+
+ def manifest
+ record do |m|
+ # Check for class naming collisions.
+ m.class_collisions class_path, class_name, "#{class_name}Test"
+
+ # Model, test, and fixture directories.
+ m.directory File.join('app/models', class_path)
+ m.directory File.join('test/unit', class_path)
+ m.directory File.join('test/fixtures', class_path)
+
+ # Model class, unit test, and fixtures.
+ m.template 'model.rb.erb', File.join('app/models', class_path, "#{file_name}.rb")
+ m.template 'unit_test.rb.erb', File.join('test/unit', class_path, "#{file_name}_test.rb")
+
+ unless options[:skip_fixture]
+ m.template 'fixtures.yml', File.join('test/fixtures', "#{table_name}.yml")
+ end
+
+ unless options[:skip_migration]
+ m.migration_template 'migration.rb.erb', 'db/migrate', :assigns => {
+ :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
+ }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
+ end
+ end
+ end
+end
--- /dev/null
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+one:
+ id: 1
+<% for attribute in attributes -%>
+ <%= attribute.name %>: <%= attribute.default %>
+<% end -%>
+two:
+ id: 2
+<% for attribute in attributes -%>
+ <%= attribute.name %>: <%= attribute.default %>
+<% end -%>
--- /dev/null
+class <%= migration_name %> < ActiveRecord::Migration
+ def self.up
+ create_table :<%= table_name %> do |t|
+<% for attribute in attributes -%>
+ t.column :<%= attribute.name %>, :<%= attribute.type %>
+<% end -%>
+ end
+ end
+
+ def self.down
+ drop_table :<%= table_name %>
+ end
+end
--- /dev/null
+class <%= class_name %> < ActiveRecord::Base
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class <%= class_name %>Test < ActiveSupport::TestCase
+ fixtures :<%= table_name %>
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
--- /dev/null
+# vim:ts=4:sw=4:
+# = RedCloth - Textile and Markdown Hybrid for Ruby
+#
+# Homepage:: http://whytheluckystiff.net/ruby/redcloth/
+# Author:: why the lucky stiff (http://whytheluckystiff.net/)
+# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
+# License:: BSD
+#
+# (see http://hobix.com/textile/ for a Textile Reference.)
+#
+# Based on (and also inspired by) both:
+#
+# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
+# Textism for PHP: http://www.textism.com/tools/textile/
+#
+#
+
+# = RedCloth
+#
+# RedCloth is a Ruby library for converting Textile and/or Markdown
+# into HTML. You can use either format, intermingled or separately.
+# You can also extend RedCloth to honor your own custom text stylings.
+#
+# RedCloth users are encouraged to use Textile if they are generating
+# HTML and to use Markdown if others will be viewing the plain text.
+#
+# == What is Textile?
+#
+# Textile is a simple formatting style for text
+# documents, loosely based on some HTML conventions.
+#
+# == Sample Textile Text
+#
+# h2. This is a title
+#
+# h3. This is a subhead
+#
+# This is a bit of paragraph.
+#
+# bq. This is a blockquote.
+#
+# = Writing Textile
+#
+# A Textile document consists of paragraphs. Paragraphs
+# can be specially formatted by adding a small instruction
+# to the beginning of the paragraph.
+#
+# h[n]. Header of size [n].
+# bq. Blockquote.
+# # Numeric list.
+# * Bulleted list.
+#
+# == Quick Phrase Modifiers
+#
+# Quick phrase modifiers are also included, to allow formatting
+# of small portions of text within a paragraph.
+#
+# \_emphasis\_
+# \_\_italicized\_\_
+# \*strong\*
+# \*\*bold\*\*
+# ??citation??
+# -deleted text-
+# +inserted text+
+# ^superscript^
+# ~subscript~
+# @code@
+# %(classname)span%
+#
+# ==notextile== (leave text alone)
+#
+# == Links
+#
+# To make a hypertext link, put the link text in "quotation
+# marks" followed immediately by a colon and the URL of the link.
+#
+# Optional: text in (parentheses) following the link text,
+# but before the closing quotation mark, will become a Title
+# attribute for the link, visible as a tool tip when a cursor is above it.
+#
+# Example:
+#
+# "This is a link (This is a title) ":http://www.textism.com
+#
+# Will become:
+#
+# <a href="http://www.textism.com" title="This is a title">This is a link</a>
+#
+# == Images
+#
+# To insert an image, put the URL for the image inside exclamation marks.
+#
+# Optional: text that immediately follows the URL in (parentheses) will
+# be used as the Alt text for the image. Images on the web should always
+# have descriptive Alt text for the benefit of readers using non-graphical
+# browsers.
+#
+# Optional: place a colon followed by a URL immediately after the
+# closing ! to make the image into a link.
+#
+# Example:
+#
+# !http://www.textism.com/common/textist.gif(Textist)!
+#
+# Will become:
+#
+# <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
+#
+# With a link:
+#
+# !/common/textist.gif(Textist)!:http://textism.com
+#
+# Will become:
+#
+# <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
+#
+# == Defining Acronyms
+#
+# HTML allows authors to define acronyms via the tag. The definition appears as a
+# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
+# this should be used at least once for each acronym in documents where they appear.
+#
+# To quickly define an acronym in Textile, place the full text in (parentheses)
+# immediately following the acronym.
+#
+# Example:
+#
+# ACLU(American Civil Liberties Union)
+#
+# Will become:
+#
+# <acronym title="American Civil Liberties Union">ACLU</acronym>
+#
+# == Adding Tables
+#
+# In Textile, simple tables can be added by seperating each column by
+# a pipe.
+#
+# |a|simple|table|row|
+# |And|Another|table|row|
+#
+# Attributes are defined by style definitions in parentheses.
+#
+# table(border:1px solid black).
+# (background:#ddd;color:red). |{}| | | |
+#
+# == Using RedCloth
+#
+# RedCloth is simply an extension of the String class, which can handle
+# Textile formatting. Use it like a String and output HTML with its
+# RedCloth#to_html method.
+#
+# doc = RedCloth.new "
+#
+# h2. Test document
+#
+# Just a simple test."
+#
+# puts doc.to_html
+#
+# By default, RedCloth uses both Textile and Markdown formatting, with
+# Textile formatting taking precedence. If you want to turn off Markdown
+# formatting, to boost speed and limit the processor:
+#
+# class RedCloth::Textile.new( str )
+
+class RedCloth3 < String
+
+ VERSION = '3.0.4'
+ DEFAULT_RULES = [:textile, :markdown]
+
+ #
+ # Two accessor for setting security restrictions.
+ #
+ # This is a nice thing if you're using RedCloth for
+ # formatting in public places (e.g. Wikis) where you
+ # don't want users to abuse HTML for bad things.
+ #
+ # If +:filter_html+ is set, HTML which wasn't
+ # created by the Textile processor will be escaped.
+ #
+ # If +:filter_styles+ is set, it will also disable
+ # the style markup specifier. ('{color: red}')
+ #
+ attr_accessor :filter_html, :filter_styles
+
+ #
+ # Accessor for toggling hard breaks.
+ #
+ # If +:hard_breaks+ is set, single newlines will
+ # be converted to HTML break tags. This is the
+ # default behavior for traditional RedCloth.
+ #
+ attr_accessor :hard_breaks
+
+ # Accessor for toggling lite mode.
+ #
+ # In lite mode, block-level rules are ignored. This means
+ # that tables, paragraphs, lists, and such aren't available.
+ # Only the inline markup for bold, italics, entities and so on.
+ #
+ # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
+ # r.to_html
+ # #=> "And then? She <strong>fell</strong>!"
+ #
+ attr_accessor :lite_mode
+
+ #
+ # Accessor for toggling span caps.
+ #
+ # Textile places `span' tags around capitalized
+ # words by default, but this wreaks havoc on Wikis.
+ # If +:no_span_caps+ is set, this will be
+ # suppressed.
+ #
+ attr_accessor :no_span_caps
+
+ #
+ # Establishes the markup predence. Available rules include:
+ #
+ # == Textile Rules
+ #
+ # The following textile rules can be set individually. Or add the complete
+ # set of rules with the single :textile rule, which supplies the rule set in
+ # the following precedence:
+ #
+ # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
+ # block_textile_table:: Textile table block structures
+ # block_textile_lists:: Textile list structures
+ # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
+ # inline_textile_image:: Textile inline images
+ # inline_textile_link:: Textile inline links
+ # inline_textile_span:: Textile inline spans
+ # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
+ #
+ # == Markdown
+ #
+ # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
+ # block_markdown_setext:: Markdown setext headers
+ # block_markdown_atx:: Markdown atx headers
+ # block_markdown_rule:: Markdown horizontal rules
+ # block_markdown_bq:: Markdown blockquotes
+ # block_markdown_lists:: Markdown lists
+ # inline_markdown_link:: Markdown links
+ attr_accessor :rules
+
+ # Returns a new RedCloth object, based on _string_ and
+ # enforcing all the included _restrictions_.
+ #
+ # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
+ # r.to_html
+ # #=>"<h1>A <b>bold</b> man</h1>"
+ #
+ def initialize( string, restrictions = [] )
+ restrictions.each { |r| method( "#{ r }=" ).call( true ) }
+ super( string )
+ end
+
+ #
+ # Generates HTML from the Textile contents.
+ #
+ # r = RedCloth.new( "And then? She *fell*!" )
+ # r.to_html( true )
+ # #=>"And then? She <strong>fell</strong>!"
+ #
+ def to_html( *rules )
+ rules = DEFAULT_RULES if rules.empty?
+ # make our working copy
+ text = self.dup
+
+ @urlrefs = {}
+ @shelf = []
+ textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
+ :block_textile_prefix, :inline_textile_image, :inline_textile_link,
+ :inline_textile_code, :inline_textile_span, :glyphs_textile]
+ markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
+ :block_markdown_bq, :block_markdown_lists,
+ :inline_markdown_reflink, :inline_markdown_link]
+ @rules = rules.collect do |rule|
+ case rule
+ when :markdown
+ markdown_rules
+ when :textile
+ textile_rules
+ else
+ rule
+ end
+ end.flatten
+
+ # standard clean up
+ incoming_entities text
+ clean_white_space text
+
+ # start processor
+ @pre_list = []
+ rip_offtags text
+ no_textile text
+ escape_html_tags text
+ hard_break text
+ unless @lite_mode
+ refs text
+ # need to do this before text is split by #blocks
+ block_textile_quotes text
+ blocks text
+ end
+ inline text
+ smooth_offtags text
+
+ retrieve text
+
+ text.gsub!( /<\/?notextile>/, '' )
+ text.gsub!( /x%x%/, '&' )
+ clean_html text if filter_html
+ text.strip!
+ text
+
+ end
+
+ #######
+ private
+ #######
+ #
+ # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
+ # (from PyTextile)
+ #
+ TEXTILE_TAGS =
+
+ [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
+ [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
+ [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
+ [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
+ [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
+
+ collect! do |a, b|
+ [a.chr, ( b.zero? and "" or "&#{ b };" )]
+ end
+
+ #
+ # Regular expressions to convert to HTML.
+ #
+ A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
+ A_VLGN = /[\-^~]/
+ C_CLAS = '(?:\([^)]+\))'
+ C_LNGE = '(?:\[[^\[\]]+\])'
+ C_STYL = '(?:\{[^}]+\})'
+ S_CSPN = '(?:\\\\\d+)'
+ S_RSPN = '(?:/\d+)'
+ A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
+ S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
+ C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
+ # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
+ PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
+ PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
+ PUNCT_Q = Regexp::quote( '*-_+^~%' )
+ HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
+
+ # Text markup tags, don't conflict with block tags
+ SIMPLE_HTML_TAGS = [
+ 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
+ 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
+ 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
+ ]
+
+ QTAGS = [
+ ['**', 'b', :limit],
+ ['*', 'strong', :limit],
+ ['??', 'cite', :limit],
+ ['-', 'del', :limit],
+ ['__', 'i', :limit],
+ ['_', 'em', :limit],
+ ['%', 'span', :limit],
+ ['+', 'ins', :limit],
+ ['^', 'sup', :limit],
+ ['~', 'sub', :limit]
+ ]
+ QTAGS.collect! do |rc, ht, rtype|
+ rcq = Regexp::quote rc
+ re =
+ case rtype
+ when :limit
+ /(^|[>\s\(])
+ (#{rcq})
+ (#{C})
+ (?::(\S+?))?
+ (\w|[^\s\-].*?[^\s\-])
+ #{rcq}
+ (?=[[:punct:]]|\s|\)|$)/x
+ else
+ /(#{rcq})
+ (#{C})
+ (?::(\S+))?
+ (\w|[^\s\-].*?[^\s\-])
+ #{rcq}/xm
+ end
+ [rc, ht, re, rtype]
+ end
+
+ # Elements to handle
+ GLYPHS = [
+ # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
+ # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
+ # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
+ # [ /\'/, '‘' ], # single opening
+ # [ /</, '<' ], # less-than
+ # [ />/, '>' ], # greater-than
+ # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
+ # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
+ # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
+ # [ /"/, '“' ], # double opening
+ # [ /\b( )?\.{3}/, '\1…' ], # ellipsis
+ # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
+ # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
+ # [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
+ # [ /\s->\s/, ' → ' ], # right arrow
+ # [ /\s-\s/, ' – ' ], # en dash
+ # [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
+ # [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
+ # [ /\b ?[(\[]R[\])]/i, '®' ], # registered
+ # [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
+ ]
+
+ H_ALGN_VALS = {
+ '<' => 'left',
+ '=' => 'center',
+ '>' => 'right',
+ '<>' => 'justify'
+ }
+
+ V_ALGN_VALS = {
+ '^' => 'top',
+ '-' => 'middle',
+ '~' => 'bottom'
+ }
+
+ #
+ # Flexible HTML escaping
+ #
+ def htmlesc( str, mode=:Quotes )
+ if str
+ str.gsub!( '&', '&' )
+ str.gsub!( '"', '"' ) if mode != :NoQuotes
+ str.gsub!( "'", ''' ) if mode == :Quotes
+ str.gsub!( '<', '<')
+ str.gsub!( '>', '>')
+ end
+ str
+ end
+
+ # Search and replace for Textile glyphs (quotes, dashes, other symbols)
+ def pgl( text )
+ #GLYPHS.each do |re, resub, tog|
+ # next if tog and method( tog ).call
+ # text.gsub! re, resub
+ #end
+ text.gsub!(/\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/) do |m|
+ "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
+ end
+ end
+
+ # Parses Textile attribute lists and builds an HTML attribute string
+ def pba( text_in, element = "" )
+
+ return '' unless text_in
+
+ style = []
+ text = text_in.dup
+ if element == 'td'
+ colspan = $1 if text =~ /\\(\d+)/
+ rowspan = $1 if text =~ /\/(\d+)/
+ style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
+ end
+
+ style << "#{ htmlesc $1 };" if text.sub!( /\{([^}]*)\}/, '' ) && !filter_styles
+
+ lang = $1 if
+ text.sub!( /\[([^)]+?)\]/, '' )
+
+ cls = $1 if
+ text.sub!( /\(([^()]+?)\)/, '' )
+
+ style << "padding-left:#{ $1.length }em;" if
+ text.sub!( /([(]+)/, '' )
+
+ style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
+
+ style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
+
+ cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
+
+ atts = ''
+ atts << " style=\"#{ style.join }\"" unless style.empty?
+ atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
+ atts << " lang=\"#{ lang }\"" if lang
+ atts << " id=\"#{ id }\"" if id
+ atts << " colspan=\"#{ colspan }\"" if colspan
+ atts << " rowspan=\"#{ rowspan }\"" if rowspan
+
+ atts
+ end
+
+ TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
+
+ # Parses a Textile table block, building HTML from the result.
+ def block_textile_table( text )
+ text.gsub!( TABLE_RE ) do |matches|
+
+ tatts, fullrow = $~[1..2]
+ tatts = pba( tatts, 'table' )
+ tatts = shelve( tatts ) if tatts
+ rows = []
+
+ fullrow.each_line do |row|
+ ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
+ cells = []
+ row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
+ next if cell == '|'
+ ctyp = 'd'
+ ctyp = 'h' if cell =~ /^_/
+
+ catts = ''
+ catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
+
+ catts = shelve( catts ) if catts
+ cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
+ end
+ ratts = shelve( ratts ) if ratts
+ rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
+ end
+ "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
+ end
+ end
+
+ LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
+ LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
+
+ # Parses Textile lists and generates HTML
+ def block_textile_lists( text )
+ text.gsub!( LISTS_RE ) do |match|
+ lines = match.split( /\n/ )
+ last_line = -1
+ depth = []
+ lines.each_with_index do |line, line_id|
+ if line =~ LISTS_CONTENT_RE
+ tl,atts,content = $~[1..3]
+ if depth.last
+ if depth.last.length > tl.length
+ (depth.length - 1).downto(0) do |i|
+ break if depth[i].length == tl.length
+ lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
+ depth.pop
+ end
+ end
+ if depth.last and depth.last.length == tl.length
+ lines[line_id - 1] << '</li>'
+ end
+ end
+ unless depth.last == tl
+ depth << tl
+ atts = pba( atts )
+ atts = shelve( atts ) if atts
+ lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
+ else
+ lines[line_id] = "\t\t<li>#{ content }"
+ end
+ last_line = line_id
+
+ else
+ last_line = line_id
+ end
+ if line_id - last_line > 1 or line_id == lines.length - 1
+ depth.delete_if do |v|
+ lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
+ end
+ end
+ end
+ lines.join( "\n" )
+ end
+ end
+
+ QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
+ QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
+
+ def block_textile_quotes( text )
+ text.gsub!( QUOTES_RE ) do |match|
+ lines = match.split( /\n/ )
+ quotes = ''
+ indent = 0
+ lines.each do |line|
+ line =~ QUOTES_CONTENT_RE
+ bq,content = $1, $2
+ l = bq.count('>')
+ if l != indent
+ quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
+ indent = l
+ end
+ quotes << (content + "\n")
+ end
+ quotes << ("\n" + '</blockquote>' * indent + "\n\n")
+ quotes
+ end
+ end
+
+ CODE_RE = /(\W)
+ @
+ (?:\|(\w+?)\|)?
+ (.+?)
+ @
+ (?=\W)/x
+
+ def inline_textile_code( text )
+ text.gsub!( CODE_RE ) do |m|
+ before,lang,code,after = $~[1..4]
+ lang = " lang=\"#{ lang }\"" if lang
+ rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
+ end
+ end
+
+ def lT( text )
+ text =~ /\#$/ ? 'o' : 'u'
+ end
+
+ def hard_break( text )
+ text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
+ end
+
+ BLOCKS_GROUP_RE = /\n{2,}(?! )/m
+
+ def blocks( text, deep_code = false )
+ text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
+ plain = blk !~ /\A[#*> ]/
+
+ # skip blocks that are complex HTML
+ if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
+ blk
+ else
+ # search for indentation levels
+ blk.strip!
+ if blk.empty?
+ blk
+ else
+ code_blk = nil
+ blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
+ flush_left iblk
+ blocks iblk, plain
+ iblk.gsub( /^(\S)/, "\t\\1" )
+ if plain
+ code_blk = iblk; ""
+ else
+ iblk
+ end
+ end
+
+ block_applied = 0
+ @rules.each do |rule_name|
+ block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
+ end
+ if block_applied.zero?
+ if deep_code
+ blk = "\t<pre><code>#{ blk }</code></pre>"
+ else
+ blk = "\t<p>#{ blk }</p>"
+ end
+ end
+ # hard_break blk
+ blk + "\n#{ code_blk }"
+ end
+ end
+
+ end.join( "\n\n" ) )
+ end
+
+ def textile_bq( tag, atts, cite, content )
+ cite, cite_title = check_refs( cite )
+ cite = " cite=\"#{ cite }\"" if cite
+ atts = shelve( atts ) if atts
+ "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
+ end
+
+ def textile_p( tag, atts, cite, content )
+ atts = shelve( atts ) if atts
+ "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
+ end
+
+ alias textile_h1 textile_p
+ alias textile_h2 textile_p
+ alias textile_h3 textile_p
+ alias textile_h4 textile_p
+ alias textile_h5 textile_p
+ alias textile_h6 textile_p
+
+ def textile_fn_( tag, num, atts, cite, content )
+ atts << " id=\"fn#{ num }\" class=\"footnote\""
+ content = "<sup>#{ num }</sup> #{ content }"
+ atts = shelve( atts ) if atts
+ "\t<p#{ atts }>#{ content }</p>"
+ end
+
+ BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
+
+ def block_textile_prefix( text )
+ if text =~ BLOCK_RE
+ tag,tagpre,num,atts,cite,content = $~[1..6]
+ atts = pba( atts )
+
+ # pass to prefix handler
+ if respond_to? "textile_#{ tag }", true
+ text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
+ elsif respond_to? "textile_#{ tagpre }_", true
+ text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
+ end
+ end
+ end
+
+ SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
+ def block_markdown_setext( text )
+ if text =~ SETEXT_RE
+ tag = if $2 == "="; "h1"; else; "h2"; end
+ blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
+ blocks cont
+ text.replace( blk + cont )
+ end
+ end
+
+ ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #'s (not counted)
+ $/x
+ def block_markdown_atx( text )
+ if text =~ ATX_RE
+ tag = "h#{ $1.length }"
+ blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
+ blocks cont
+ text.replace( blk + cont )
+ end
+ end
+
+ MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
+
+ def block_markdown_bq( text )
+ text.gsub!( MARKDOWN_BQ_RE ) do |blk|
+ blk.gsub!( /^ *> ?/, '' )
+ flush_left blk
+ blocks blk
+ blk.gsub!( /^(\S)/, "\t\\1" )
+ "<blockquote>\n#{ blk }\n</blockquote>\n\n"
+ end
+ end
+
+ MARKDOWN_RULE_RE = /^(#{
+ ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
+ })$/
+
+ def block_markdown_rule( text )
+ text.gsub!( MARKDOWN_RULE_RE ) do |blk|
+ "<hr />"
+ end
+ end
+
+ # XXX TODO XXX
+ def block_markdown_lists( text )
+ end
+
+ def inline_textile_span( text )
+ QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
+ text.gsub!( qtag_re ) do |m|
+
+ case rtype
+ when :limit
+ sta,qtag,atts,cite,content = $~[1..5]
+ else
+ qtag,atts,cite,content = $~[1..4]
+ sta = ''
+ end
+ atts = pba( atts )
+ atts << " cite=\"#{ cite }\"" if cite
+ atts = shelve( atts ) if atts
+
+ "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
+
+ end
+ end
+ end
+
+ LINK_RE = /
+ (
+ ([\s\[{(]|[#{PUNCT}])? # $pre
+ " # start
+ (#{C}) # $atts
+ ([^"\n]+?) # $text
+ \s?
+ (?:\(([^)]+?)\)(?="))? # $title
+ ":
+ ( # $url
+ (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
+ [\w\/]\S+?
+ )
+ (\/)? # $slash
+ ([^\w\=\/;\(\)]*?) # $post
+ )
+ (?=<|\s|$)
+ /x
+#"
+ def inline_textile_link( text )
+ text.gsub!( LINK_RE ) do |m|
+ all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
+ if text.include?('<br />')
+ all
+ else
+ url, url_title = check_refs( url )
+ title ||= url_title
+
+ # Idea below : an URL with unbalanced parethesis and
+ # ending by ')' is put into external parenthesis
+ if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
+ url=url[0..-2] # discard closing parenth from url
+ post = ")"+post # add closing parenth to post
+ end
+ atts = pba( atts )
+ atts = " href=\"#{ url }#{ slash }\"#{ atts }"
+ atts << " title=\"#{ htmlesc title }\"" if title
+ atts = shelve( atts ) if atts
+
+ external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
+
+ "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
+ end
+ end
+ end
+
+ MARKDOWN_REFLINK_RE = /
+ \[([^\[\]]+)\] # $text
+ [ ]? # opt. space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+ \[(.*?)\] # $id
+ /x
+
+ def inline_markdown_reflink( text )
+ text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
+ text, id = $~[1..2]
+
+ if id.empty?
+ url, title = check_refs( text )
+ else
+ url, title = check_refs( id )
+ end
+
+ atts = " href=\"#{ url }\""
+ atts << " title=\"#{ title }\"" if title
+ atts = shelve( atts )
+
+ "<a#{ atts }>#{ text }</a>"
+ end
+ end
+
+ MARKDOWN_LINK_RE = /
+ \[([^\[\]]+)\] # $text
+ \( # open paren
+ [ \t]* # opt space
+ <?(.+?)>? # $href
+ [ \t]* # opt space
+ (?: # whole title
+ (['"]) # $quote
+ (.*?) # $title
+ \3 # matching quote
+ )? # title is optional
+ \)
+ /x
+
+ def inline_markdown_link( text )
+ text.gsub!( MARKDOWN_LINK_RE ) do |m|
+ text, url, quote, title = $~[1..4]
+
+ atts = " href=\"#{ url }\""
+ atts << " title=\"#{ title }\"" if title
+ atts = shelve( atts )
+
+ "<a#{ atts }>#{ text }</a>"
+ end
+ end
+
+ TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
+ MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
+
+ def refs( text )
+ @rules.each do |rule_name|
+ method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
+ end
+ end
+
+ def refs_textile( text )
+ text.gsub!( TEXTILE_REFS_RE ) do |m|
+ flag, url = $~[2..3]
+ @urlrefs[flag.downcase] = [url, nil]
+ nil
+ end
+ end
+
+ def refs_markdown( text )
+ text.gsub!( MARKDOWN_REFS_RE ) do |m|
+ flag, url = $~[2..3]
+ title = $~[6]
+ @urlrefs[flag.downcase] = [url, title]
+ nil
+ end
+ end
+
+ def check_refs( text )
+ ret = @urlrefs[text.downcase] if text
+ ret || [text, nil]
+ end
+
+ IMAGE_RE = /
+ (>|\s|^) # start of line?
+ \! # opening
+ (\<|\=|\>)? # optional alignment atts
+ (#{C}) # optional style,class atts
+ (?:\. )? # optional dot-space
+ ([^\s(!]+?) # presume this is the src
+ \s? # optional space
+ (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
+ \! # closing
+ (?::#{ HYPERLINK })? # optional href
+ /x
+
+ def inline_textile_image( text )
+ text.gsub!( IMAGE_RE ) do |m|
+ stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
+ htmlesc title
+ atts = pba( atts )
+ atts = " src=\"#{ url }\"#{ atts }"
+ atts << " title=\"#{ title }\"" if title
+ atts << " alt=\"#{ title }\""
+ # size = @getimagesize($url);
+ # if($size) $atts.= " $size[3]";
+
+ href, alt_title = check_refs( href ) if href
+ url, url_title = check_refs( url )
+
+ out = ''
+ out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
+ out << "<img#{ shelve( atts ) } />"
+ out << "</a>#{ href_a1 }#{ href_a2 }" if href
+
+ if algn
+ algn = h_align( algn )
+ if stln == "<p>"
+ out = "<p style=\"float:#{ algn }\">#{ out }"
+ else
+ out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
+ end
+ else
+ out = stln + out
+ end
+
+ out
+ end
+ end
+
+ def shelve( val )
+ @shelf << val
+ " :redsh##{ @shelf.length }:"
+ end
+
+ def retrieve( text )
+ @shelf.each_with_index do |r, i|
+ text.gsub!( " :redsh##{ i + 1 }:", r )
+ end
+ end
+
+ def incoming_entities( text )
+ ## turn any incoming ampersands into a dummy character for now.
+ ## This uses a negative lookahead for alphanumerics followed by a semicolon,
+ ## implying an incoming html entity, to be skipped
+
+ text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
+ end
+
+ def no_textile( text )
+ text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
+ '\1<notextile>\2</notextile>\3' )
+ text.gsub!( /^ *==([^=]+.*?)==/m,
+ '\1<notextile>\2</notextile>\3' )
+ end
+
+ def clean_white_space( text )
+ # normalize line breaks
+ text.gsub!( /\r\n/, "\n" )
+ text.gsub!( /\r/, "\n" )
+ text.gsub!( /\t/, ' ' )
+ text.gsub!( /^ +$/, '' )
+ text.gsub!( /\n{3,}/, "\n\n" )
+ text.gsub!( /"$/, "\" " )
+
+ # if entire document is indented, flush
+ # to the left side
+ flush_left text
+ end
+
+ def flush_left( text )
+ indt = 0
+ if text =~ /^ /
+ while text !~ /^ {#{indt}}\S/
+ indt += 1
+ end unless text.empty?
+ if indt.nonzero?
+ text.gsub!( /^ {#{indt}}/, '' )
+ end
+ end
+ end
+
+ def footnote_ref( text )
+ text.gsub!( /\b\[([0-9]+?)\](\s)?/,
+ '<sup><a href="#fn\1">\1</a></sup>\2' )
+ end
+
+ OFFTAGS = /(code|pre|kbd|notextile)/
+ OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
+ OFFTAG_OPEN = /<#{ OFFTAGS }/
+ OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
+ HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
+ ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
+
+ def glyphs_textile( text, level = 0 )
+ if text !~ HASTAG_MATCH
+ pgl text
+ footnote_ref text
+ else
+ codepre = 0
+ text.gsub!( ALLTAG_MATCH ) do |line|
+ ## matches are off if we're between <code>, <pre> etc.
+ if $1
+ if line =~ OFFTAG_OPEN
+ codepre += 1
+ elsif line =~ OFFTAG_CLOSE
+ codepre -= 1
+ codepre = 0 if codepre < 0
+ end
+ elsif codepre.zero?
+ glyphs_textile( line, level + 1 )
+ else
+ htmlesc( line, :NoQuotes )
+ end
+ # p [level, codepre, line]
+
+ line
+ end
+ end
+ end
+
+ def rip_offtags( text )
+ if text =~ /<.*>/
+ ## strip and encode <pre> content
+ codepre, used_offtags = 0, {}
+ text.gsub!( OFFTAG_MATCH ) do |line|
+ if $3
+ offtag, aftertag = $4, $5
+ codepre += 1
+ used_offtags[offtag] = true
+ if codepre - used_offtags.length > 0
+ htmlesc( line, :NoQuotes )
+ @pre_list.last << line
+ line = ""
+ else
+ htmlesc( aftertag, :NoQuotes ) if aftertag
+ line = "<redpre##{ @pre_list.length }>"
+ $3.match(/<#{ OFFTAGS }([^>]*)>/)
+ tag = $1
+ $2.to_s.match(/(class\=\S+)/i)
+ tag << " #{$1}" if $1
+ @pre_list << "<#{ tag }>#{ aftertag }"
+ end
+ elsif $1 and codepre > 0
+ if codepre - used_offtags.length > 0
+ htmlesc( line, :NoQuotes )
+ @pre_list.last << line
+ line = ""
+ end
+ codepre -= 1 unless codepre.zero?
+ used_offtags = {} if codepre.zero?
+ end
+ line
+ end
+ end
+ text
+ end
+
+ def smooth_offtags( text )
+ unless @pre_list.empty?
+ ## replace <pre> content
+ text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
+ end
+ end
+
+ def inline( text )
+ [/^inline_/, /^glyphs_/].each do |meth_re|
+ @rules.each do |rule_name|
+ method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
+ end
+ end
+ end
+
+ def h_align( text )
+ H_ALGN_VALS[text]
+ end
+
+ def v_align( text )
+ V_ALGN_VALS[text]
+ end
+
+ def textile_popup_help( name, windowW, windowH )
+ ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
+ end
+
+ # HTML cleansing stuff
+ BASIC_TAGS = {
+ 'a' => ['href', 'title'],
+ 'img' => ['src', 'alt', 'title'],
+ 'br' => [],
+ 'i' => nil,
+ 'u' => nil,
+ 'b' => nil,
+ 'pre' => nil,
+ 'kbd' => nil,
+ 'code' => ['lang'],
+ 'cite' => nil,
+ 'strong' => nil,
+ 'em' => nil,
+ 'ins' => nil,
+ 'sup' => nil,
+ 'sub' => nil,
+ 'del' => nil,
+ 'table' => nil,
+ 'tr' => nil,
+ 'td' => ['colspan', 'rowspan'],
+ 'th' => nil,
+ 'ol' => nil,
+ 'ul' => nil,
+ 'li' => nil,
+ 'p' => nil,
+ 'h1' => nil,
+ 'h2' => nil,
+ 'h3' => nil,
+ 'h4' => nil,
+ 'h5' => nil,
+ 'h6' => nil,
+ 'blockquote' => ['cite']
+ }
+
+ def clean_html( text, tags = BASIC_TAGS )
+ text.gsub!( /<!\[CDATA\[/, '' )
+ text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
+ raw = $~
+ tag = raw[2].downcase
+ if tags.has_key? tag
+ pcs = [tag]
+ tags[tag].each do |prop|
+ ['"', "'", ''].each do |q|
+ q2 = ( q != '' ? q : '\s' )
+ if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
+ attrv = $1
+ next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
+ pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
+ break
+ end
+ end
+ end if tags[tag]
+ "<#{raw[1]}#{pcs.join " "}>"
+ else
+ " "
+ end
+ end
+ end
+
+ ALLOWED_TAGS = %w(redpre pre code notextile)
+
+ def escape_html_tags(text)
+ text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" }
+ end
+end
+
--- /dev/null
+require 'redmine/access_control'
+require 'redmine/menu_manager'
+require 'redmine/activity'
+require 'redmine/mime_type'
+require 'redmine/core_ext'
+require 'redmine/themes'
+require 'redmine/hook'
+require 'redmine/plugin'
+require 'redmine/wiki_formatting'
+
+begin
+ require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
+rescue LoadError
+ # RMagick is not available
+end
+
+if RUBY_VERSION < '1.9'
+ require 'faster_csv'
+else
+ require 'csv'
+ FCSV = CSV
+end
+
+REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
+
+# Permissions
+Redmine::AccessControl.map do |map|
+ map.permission :view_project, {:projects => [:show, :activity]}, :public => true
+ map.permission :search_project, {:search => :index}, :public => true
+ map.permission :add_project, {:projects => :add}, :require => :loggedin
+ map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
+ map.permission :select_project_modules, {:projects => :modules}, :require => :member
+ map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
+ map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :close_completed, :destroy]}, :require => :member
+
+ map.project_module :issue_tracking do |map|
+ # Issue categories
+ map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
+ # Issues
+ map.permission :view_issues, {:projects => [:changelog, :roadmap],
+ :issues => [:index, :changes, :show, :context_menu],
+ :versions => [:show, :status_by],
+ :queries => :index,
+ :reports => :issue_report}
+ map.permission :add_issues, {:issues => :new}
+ map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
+ map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
+ map.permission :add_issue_notes, {:issues => [:edit, :reply]}
+ map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
+ map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
+ map.permission :move_issues, {:issues => :move}, :require => :loggedin
+ map.permission :delete_issues, {:issues => :destroy}, :require => :member
+ # Queries
+ map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
+ map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
+ # Gantt & calendar
+ map.permission :view_gantt, :issues => :gantt
+ map.permission :view_calendar, :issues => :calendar
+ # Watchers
+ map.permission :view_issue_watchers, {}
+ map.permission :add_issue_watchers, {:watchers => :new}
+ map.permission :delete_issue_watchers, {:watchers => :destroy}
+ end
+
+ map.project_module :time_tracking do |map|
+ map.permission :log_time, {:timelog => :edit}, :require => :loggedin
+ map.permission :view_time_entries, :timelog => [:details, :report]
+ map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
+ map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
+ map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
+ end
+
+ map.project_module :news do |map|
+ map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
+ map.permission :view_news, {:news => [:index, :show]}, :public => true
+ map.permission :comment_news, {:news => :add_comment}
+ end
+
+ map.project_module :documents do |map|
+ map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
+ map.permission :view_documents, :documents => [:index, :show, :download]
+ end
+
+ map.project_module :files do |map|
+ map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
+ map.permission :view_files, :projects => :list_files, :versions => :download
+ end
+
+ map.project_module :wiki do |map|
+ map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
+ map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
+ map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
+ map.permission :view_wiki_pages, :wiki => [:index, :special]
+ map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
+ map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
+ map.permission :delete_wiki_pages_attachments, {}
+ map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
+ end
+
+ map.project_module :repository do |map|
+ map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
+ map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
+ map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
+ map.permission :commit_access, {}
+ end
+
+ map.project_module :boards do |map|
+ map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
+ map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
+ map.permission :add_messages, {:messages => [:new, :reply, :quote]}
+ map.permission :edit_messages, {:messages => :edit}, :require => :member
+ map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
+ map.permission :delete_messages, {:messages => :destroy}, :require => :member
+ map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
+ end
+end
+
+Redmine::MenuManager.map :top_menu do |menu|
+ menu.push :home, :home_path
+ menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
+ menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
+ menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
+ menu.push :help, Redmine::Info.help_url, :last => true
+end
+
+Redmine::MenuManager.map :account_menu do |menu|
+ menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
+ menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
+ menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
+ menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
+end
+
+Redmine::MenuManager.map :application_menu do |menu|
+ # Empty
+end
+
+Redmine::MenuManager.map :admin_menu do |menu|
+ # Empty
+end
+
+Redmine::MenuManager.map :project_menu do |menu|
+ menu.push :overview, { :controller => 'projects', :action => 'show' }
+ menu.push :activity, { :controller => 'projects', :action => 'activity' }
+ menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
+ :if => Proc.new { |p| p.versions.any? }
+ menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
+ menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
+ :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
+ menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
+ menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
+ menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
+ :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
+ menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
+ :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
+ menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
+ menu.push :repository, { :controller => 'repositories', :action => 'show' },
+ :if => Proc.new { |p| p.repository && !p.repository.new_record? }
+ menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
+end
+
+Redmine::Activity.map do |activity|
+ activity.register :issues, :class_name => %w(Issue Journal)
+ activity.register :changesets
+ activity.register :news
+ activity.register :documents, :class_name => %w(Document Attachment)
+ activity.register :files, :class_name => 'Attachment'
+ activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
+ activity.register :messages, :default => false
+ activity.register :time_entries, :default => false
+end
+
+Redmine::WikiFormatting.map do |format|
+ format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
+end
--- /dev/null
+module Redmine
+ class About
+ def self.print_plugin_info
+ plugins = Redmine::Plugin.registered_plugins
+
+ if !plugins.empty?
+ column_with = plugins.map {|internal_name, plugin| plugin.name.length}.max
+ puts "\nAbout your Redmine plugins"
+
+ plugins.each do |internal_name, plugin|
+ puts sprintf("%-#{column_with}s %s", plugin.name, plugin.version)
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 AccessControl
+
+ class << self
+ def map
+ mapper = Mapper.new
+ yield mapper
+ @permissions ||= []
+ @permissions += mapper.mapped_permissions
+ end
+
+ def permissions
+ @permissions
+ end
+
+ # Returns the permission of given name or nil if it wasn't found
+ # Argument should be a symbol
+ def permission(name)
+ permissions.detect {|p| p.name == name}
+ end
+
+ # Returns the actions that are allowed by the permission of given name
+ def allowed_actions(permission_name)
+ perm = permission(permission_name)
+ perm ? perm.actions : []
+ end
+
+ def public_permissions
+ @public_permissions ||= @permissions.select {|p| p.public?}
+ end
+
+ def members_only_permissions
+ @members_only_permissions ||= @permissions.select {|p| p.require_member?}
+ end
+
+ def loggedin_only_permissions
+ @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
+ end
+
+ def available_project_modules
+ @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
+ end
+
+ def modules_permissions(modules)
+ @permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
+ end
+ end
+
+ class Mapper
+ def initialize
+ @project_module = nil
+ end
+
+ def permission(name, hash, options={})
+ @permissions ||= []
+ options.merge!(:project_module => @project_module)
+ @permissions << Permission.new(name, hash, options)
+ end
+
+ def project_module(name, options={})
+ @project_module = name
+ yield self
+ @project_module = nil
+ end
+
+ def mapped_permissions
+ @permissions
+ end
+ end
+
+ class Permission
+ attr_reader :name, :actions, :project_module
+
+ def initialize(name, hash, options)
+ @name = name
+ @actions = []
+ @public = options[:public] || false
+ @require = options[:require]
+ @project_module = options[:project_module]
+ hash.each do |controller, actions|
+ if actions.is_a? Array
+ @actions << actions.collect {|action| "#{controller}/#{action}"}
+ else
+ @actions << "#{controller}/#{actions}"
+ end
+ end
+ @actions.flatten!
+ end
+
+ def public?
+ @public
+ end
+
+ def require_member?
+ @require && @require == :member
+ end
+
+ def require_loggedin?
+ @require && (@require == :member || @require == :loggedin)
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 AccessKeys
+ ACCESSKEYS = {:edit => 'e',
+ :preview => 'r',
+ :quick_search => 'f',
+ :search => '4',
+ :new_issue => '7'
+ }.freeze unless const_defined?(:ACCESSKEYS)
+
+ def self.key_for(action)
+ ACCESSKEYS[action]
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Activity
+
+ mattr_accessor :available_event_types, :default_event_types, :providers
+
+ @@available_event_types = []
+ @@default_event_types = []
+ @@providers = Hash.new {|h,k| h[k]=[] }
+
+ class << self
+ def map(&block)
+ yield self
+ end
+
+ # Registers an activity provider
+ def register(event_type, options={})
+ options.assert_valid_keys(:class_name, :default)
+
+ event_type = event_type.to_s
+ providers = options[:class_name] || event_type.classify
+ providers = ([] << providers) unless providers.is_a?(Array)
+
+ @@available_event_types << event_type unless @@available_event_types.include?(event_type)
+ @@default_event_types << event_type unless options[:default] == false
+ @@providers[event_type] += providers
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Activity
+ # Class used to retrieve activity events
+ class Fetcher
+ attr_reader :user, :project, :scope
+
+ # Needs to be unloaded in development mode
+ @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
+
+ def initialize(user, options={})
+ options.assert_valid_keys(:project, :with_subprojects, :author)
+ @user = user
+ @project = options[:project]
+ @options = options
+
+ @scope = event_types
+ end
+
+ # Returns an array of available event types
+ def event_types
+ return @event_types unless @event_types.nil?
+
+ @event_types = Redmine::Activity.available_event_types
+ @event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project
+ @event_types
+ end
+
+ # Yields to filter the activity scope
+ def scope_select(&block)
+ @scope = @scope.select {|t| yield t }
+ end
+
+ # Sets the scope
+ # Argument can be :all, :default or an array of event types
+ def scope=(s)
+ case s
+ when :all
+ @scope = event_types
+ when :default
+ default_scope!
+ else
+ @scope = s & event_types
+ end
+ end
+
+ # Resets the scope to the default scope
+ def default_scope!
+ @scope = Redmine::Activity.default_event_types
+ end
+
+ # Returns an array of events for the given date range
+ # sorted in reverse chronological order
+ def events(from = nil, to = nil, options={})
+ e = []
+ @options[:limit] = options[:limit]
+
+ @scope.each do |event_type|
+ constantized_providers(event_type).each do |provider|
+ e += provider.find_events(event_type, @user, from, to, @options)
+ end
+ end
+
+ e.sort! {|a,b| b.event_datetime <=> a.event_datetime}
+
+ if options[:limit]
+ e = e.slice(0, options[:limit])
+ end
+ e
+ end
+
+ private
+
+ def constantized_providers(event_type)
+ @@constantized_providers[event_type]
+ end
+ end
+ end
+end
--- /dev/null
+Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
--- /dev/null
+require File.dirname(__FILE__) + '/string/conversions'
+require File.dirname(__FILE__) + '/string/inflections'
+
+class String #:nodoc:
+ include Redmine::CoreExtensions::String::Conversions
+ include Redmine::CoreExtensions::String::Inflections
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine #:nodoc:
+ module CoreExtensions #:nodoc:
+ module String #:nodoc:
+ # Custom string conversions
+ module Conversions
+ # Parses hours format and returns a float
+ def to_hours
+ s = self.dup
+ s.strip!
+ if s =~ %r{^(\d+([.,]\d+)?)h?$}
+ s = $1
+ else
+ # 2:30 => 2.5
+ s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
+ # 2h30, 2h, 30m => 2.5, 2, 0.5
+ s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
+ end
+ # 2,5 => 2.5
+ s.gsub!(',', '.')
+ begin; Kernel.Float(s); rescue; nil; end
+ end
+
+ # Object#to_a removed in ruby1.9
+ if RUBY_VERSION > '1.9'
+ def to_a
+ [self.dup]
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine #:nodoc:
+ module CoreExtensions #:nodoc:
+ module String #:nodoc:
+ # Custom string inflections
+ module Inflections
+ def with_leading_slash
+ starts_with?('/') ? self : "/#{ self }"
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module DefaultData
+ class DataAlreadyLoaded < Exception; end
+
+ module Loader
+ include Redmine::I18n
+
+ class << self
+ # Returns true if no data is already loaded in the database
+ # otherwise false
+ def no_data?
+ !Role.find(:first, :conditions => {:builtin => 0}) &&
+ !Tracker.find(:first) &&
+ !IssueStatus.find(:first) &&
+ !Enumeration.find(:first)
+ end
+
+ # Loads the default data
+ # Raises a RecordNotSaved exception if something goes wrong
+ def load(lang=nil)
+ raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
+ set_language_if_valid(lang)
+
+ Role.transaction do
+ # Roles
+ manager = Role.create! :name => l(:default_role_manager),
+ :position => 1
+ manager.permissions = manager.setable_permissions.collect {|p| p.name}
+ manager.save!
+
+ developper = Role.create! :name => l(:default_role_developper),
+ :position => 2,
+ :permissions => [:manage_versions,
+ :manage_categories,
+ :view_issues,
+ :add_issues,
+ :edit_issues,
+ :manage_issue_relations,
+ :add_issue_notes,
+ :save_queries,
+ :view_gantt,
+ :view_calendar,
+ :log_time,
+ :view_time_entries,
+ :comment_news,
+ :view_documents,
+ :view_wiki_pages,
+ :view_wiki_edits,
+ :edit_wiki_pages,
+ :delete_wiki_pages,
+ :add_messages,
+ :edit_own_messages,
+ :view_files,
+ :manage_files,
+ :browse_repository,
+ :view_changesets,
+ :commit_access]
+
+ reporter = Role.create! :name => l(:default_role_reporter),
+ :position => 3,
+ :permissions => [:view_issues,
+ :add_issues,
+ :add_issue_notes,
+ :save_queries,
+ :view_gantt,
+ :view_calendar,
+ :log_time,
+ :view_time_entries,
+ :comment_news,
+ :view_documents,
+ :view_wiki_pages,
+ :view_wiki_edits,
+ :add_messages,
+ :edit_own_messages,
+ :view_files,
+ :browse_repository,
+ :view_changesets]
+
+ Role.non_member.update_attribute :permissions, [:view_issues,
+ :add_issues,
+ :add_issue_notes,
+ :save_queries,
+ :view_gantt,
+ :view_calendar,
+ :view_time_entries,
+ :comment_news,
+ :view_documents,
+ :view_wiki_pages,
+ :view_wiki_edits,
+ :add_messages,
+ :view_files,
+ :browse_repository,
+ :view_changesets]
+
+ Role.anonymous.update_attribute :permissions, [:view_issues,
+ :view_gantt,
+ :view_calendar,
+ :view_time_entries,
+ :view_documents,
+ :view_wiki_pages,
+ :view_wiki_edits,
+ :view_files,
+ :browse_repository,
+ :view_changesets]
+
+ # Trackers
+ Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
+ Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
+ Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
+
+ # Issue statuses
+ new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
+ in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
+ resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
+ feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
+ closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
+ rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
+
+ # Workflow
+ Tracker.find(:all).each { |t|
+ IssueStatus.find(:all).each { |os|
+ IssueStatus.find(:all).each { |ns|
+ Workflow.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
+ }
+ }
+ }
+
+ Tracker.find(:all).each { |t|
+ [new, in_progress, resolved, feedback].each { |os|
+ [in_progress, resolved, feedback, closed].each { |ns|
+ Workflow.create!(:tracker_id => t.id, :role_id => developper.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
+ }
+ }
+ }
+
+ Tracker.find(:all).each { |t|
+ [new, in_progress, resolved, feedback].each { |os|
+ [closed].each { |ns|
+ Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
+ }
+ }
+ Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
+ }
+
+ # Enumerations
+ DocumentCategory.create!(:opt => "DCAT", :name => l(:default_doc_category_user), :position => 1)
+ DocumentCategory.create!(:opt => "DCAT", :name => l(:default_doc_category_tech), :position => 2)
+
+ IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_low), :position => 1)
+ IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_normal), :position => 2, :is_default => true)
+ IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_high), :position => 3)
+ IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_urgent), :position => 4)
+ IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_immediate), :position => 5)
+
+ TimeEntryActivity.create!(:opt => "ACTI", :name => l(:default_activity_design), :position => 1)
+ TimeEntryActivity.create!(:opt => "ACTI", :name => l(:default_activity_development), :position => 2)
+ end
+ true
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'iconv'
+require 'rfpdf/fpdf'
+require 'rfpdf/chinese'
+
+module Redmine
+ module Export
+ module PDF
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::NumberHelper
+
+ class IFPDF < FPDF
+ include Redmine::I18n
+ attr_accessor :footer_date
+
+ def initialize(lang)
+ super()
+ set_language_if_valid lang
+ case current_language.to_s.downcase
+ when 'ja'
+ extend(PDF_Japanese)
+ AddSJISFont()
+ @font_for_content = 'SJIS'
+ @font_for_footer = 'SJIS'
+ when 'zh'
+ extend(PDF_Chinese)
+ AddGBFont()
+ @font_for_content = 'GB'
+ @font_for_footer = 'GB'
+ when 'zh-tw'
+ extend(PDF_Chinese)
+ AddBig5Font()
+ @font_for_content = 'Big5'
+ @font_for_footer = 'Big5'
+ else
+ @font_for_content = 'Arial'
+ @font_for_footer = 'Helvetica'
+ end
+ SetCreator(Redmine::Info.app_name)
+ SetFont(@font_for_content)
+ end
+
+ def SetFontStyle(style, size)
+ SetFont(@font_for_content, style, size)
+ end
+
+ def SetTitle(txt)
+ txt = begin
+ utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
+ hextxt = "<FEFF" # FEFF is BOM
+ hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
+ hextxt << ">"
+ rescue
+ txt
+ end || ''
+ super(txt)
+ end
+
+ def textstring(s)
+ # Format a text string
+ if s =~ /^</ # This means the string is hex-dumped.
+ return s
+ else
+ return '('+escape(s)+')'
+ end
+ end
+
+ def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
+ @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
+ # these quotation marks are not correctly rendered in the pdf
+ txt = txt.gsub(/[“�]/, '"') if txt
+ txt = begin
+ # 0x5c char handling
+ txtar = txt.split('\\')
+ txtar << '' if txt[-1] == ?\\
+ txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
+ rescue
+ txt
+ end || ''
+ super w,h,txt,border,ln,align,fill,link
+ end
+
+ def Footer
+ SetFont(@font_for_footer, 'I', 8)
+ SetY(-15)
+ SetX(15)
+ Cell(0, 5, @footer_date, 0, 0, 'L')
+ SetY(-15)
+ SetX(-30)
+ Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
+ end
+ end
+
+ # Returns a PDF string of a list of issues
+ def issues_to_pdf(issues, project, query)
+ pdf = IFPDF.new(current_language)
+ title = query.new_record? ? l(:label_issue_plural) : query.name
+ title = "#{project} - #{title}" if project
+ pdf.SetTitle(title)
+ pdf.AliasNbPages
+ pdf.footer_date = format_date(Date.today)
+ pdf.AddPage("L")
+
+ row_height = 6
+ col_width = []
+ unless query.columns.empty?
+ col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
+ ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
+ col_width = col_width.collect {|w| w * ratio}
+ end
+
+ # title
+ pdf.SetFontStyle('B',11)
+ pdf.Cell(190,10, title)
+ pdf.Ln
+
+ # headers
+ pdf.SetFontStyle('B',8)
+ pdf.SetFillColor(230, 230, 230)
+ pdf.Cell(15, row_height, "#", 1, 0, 'L', 1)
+ query.columns.each_with_index do |column, i|
+ pdf.Cell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
+ end
+ pdf.Ln
+
+ # rows
+ pdf.SetFontStyle('',8)
+ pdf.SetFillColor(255, 255, 255)
+ group = false
+ issues.each do |issue|
+ if query.grouped? && issue.send(query.group_by) != group
+ group = issue.send(query.group_by)
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(277, row_height, "#{group.blank? ? 'None' : group.to_s}", 1, 1, 'L')
+ pdf.SetFontStyle('',8)
+ end
+ pdf.Cell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
+ query.columns.each_with_index do |column, i|
+ s = if column.is_a?(QueryCustomFieldColumn)
+ cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
+ show_value(cv)
+ else
+ value = issue.send(column.name)
+ if value.is_a?(Date)
+ format_date(value)
+ elsif value.is_a?(Time)
+ format_time(value)
+ else
+ value
+ end
+ end
+ pdf.Cell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
+ end
+ pdf.Ln
+ end
+ if issues.size == Setting.issues_export_limit.to_i
+ pdf.SetFontStyle('B',10)
+ pdf.Cell(0, row_height, '...')
+ end
+ pdf.Output
+ end
+
+ # Returns a PDF string of a single issue
+ def issue_to_pdf(issue)
+ pdf = IFPDF.new(current_language)
+ pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
+ pdf.AliasNbPages
+ pdf.footer_date = format_date(Date.today)
+ pdf.AddPage
+
+ pdf.SetFontStyle('B',11)
+ pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
+ pdf.Ln
+
+ y0 = pdf.GetY
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_status) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, issue.status.to_s,"RT")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_priority) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, issue.priority.to_s,"RT")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_author) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, issue.author.to_s,"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_category) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, issue.category.to_s,"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_created_on) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(issue.created_on),"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, issue.assigned_to.to_s,"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(issue.updated_on),"RB")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_due_date) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(issue.due_date),"RB")
+ pdf.Ln
+
+ for custom_value in issue.custom_field_values
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, (show_value custom_value),"R")
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_subject) + ":","LTB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(155,5, issue.subject,"RTB")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_description) + ":")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, @issue.description,"BR")
+
+ pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
+ pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
+ pdf.Ln
+
+ if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_associated_revisions), "B")
+ pdf.Ln
+ for changeset in issue.changesets
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
+ pdf.Ln
+ unless changeset.comments.blank?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, changeset.comments)
+ end
+ pdf.Ln
+ end
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_history), "B")
+ pdf.Ln
+ for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
+ pdf.Ln
+ pdf.SetFontStyle('I',8)
+ for detail in journal.details
+ pdf.Cell(190,5, "- " + show_detail(detail, true))
+ pdf.Ln
+ end
+ if journal.notes?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, journal.notes)
+ end
+ pdf.Ln
+ end
+
+ if issue.attachments.any?
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_attachment_plural), "B")
+ pdf.Ln
+ for attachment in issue.attachments
+ pdf.SetFontStyle('',8)
+ pdf.Cell(80,5, attachment.filename)
+ pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
+ pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
+ pdf.Cell(65,5, attachment.author.name,0,0,"R")
+ pdf.Ln
+ end
+ end
+ pdf.Output
+ end
+
+ # Returns a PDF string of a gantt chart
+ def gantt_to_pdf(gantt, project)
+ pdf = IFPDF.new(current_language)
+ pdf.SetTitle("#{l(:label_gantt)} #{project}")
+ pdf.AliasNbPages
+ pdf.footer_date = format_date(Date.today)
+ pdf.AddPage("L")
+ pdf.SetFontStyle('B',12)
+ pdf.SetX(15)
+ pdf.Cell(70, 20, project.to_s)
+ pdf.Ln
+ pdf.SetFontStyle('B',9)
+
+ subject_width = 70
+ header_heigth = 5
+
+ headers_heigth = header_heigth
+ show_weeks = false
+ show_days = false
+
+ if gantt.months < 7
+ show_weeks = true
+ headers_heigth = 2*header_heigth
+ if gantt.months < 3
+ show_days = true
+ headers_heigth = 3*header_heigth
+ end
+ end
+
+ g_width = 210
+ zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
+ g_height = 120
+ t_height = g_height + headers_heigth
+
+ y_start = pdf.GetY
+
+ # Months headers
+ month_f = gantt.date_from
+ left = subject_width
+ height = header_heigth
+ gantt.months.times do
+ width = ((month_f >> 1) - month_f) * zoom
+ pdf.SetY(y_start)
+ pdf.SetX(left)
+ pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
+ left = left + width
+ month_f = month_f >> 1
+ end
+
+ # Weeks headers
+ if show_weeks
+ left = subject_width
+ height = header_heigth
+ if gantt.date_from.cwday == 1
+ # gantt.date_from is monday
+ week_f = gantt.date_from
+ else
+ # find next monday after gantt.date_from
+ week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
+ width = (7 - gantt.date_from.cwday + 1) * zoom-1
+ pdf.SetY(y_start + header_heigth)
+ pdf.SetX(left)
+ pdf.Cell(width + 1, height, "", "LTR")
+ left = left + width+1
+ end
+ while week_f <= gantt.date_to
+ width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
+ pdf.SetY(y_start + header_heigth)
+ pdf.SetX(left)
+ pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
+ left = left + width
+ week_f = week_f+7
+ end
+ end
+
+ # Days headers
+ if show_days
+ left = subject_width
+ height = header_heigth
+ wday = gantt.date_from.cwday
+ pdf.SetFontStyle('B',7)
+ (gantt.date_to - gantt.date_from + 1).to_i.times do
+ width = zoom
+ pdf.SetY(y_start + 2 * header_heigth)
+ pdf.SetX(left)
+ pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
+ left = left + width
+ wday = wday + 1
+ wday = 1 if wday > 7
+ end
+ end
+
+ pdf.SetY(y_start)
+ pdf.SetX(15)
+ pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
+
+ # Tasks
+ top = headers_heigth + y_start
+ pdf.SetFontStyle('B',7)
+ gantt.events.each do |i|
+ pdf.SetY(top)
+ pdf.SetX(15)
+
+ if i.is_a? Issue
+ pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
+ else
+ pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
+ end
+
+ pdf.SetY(top)
+ pdf.SetX(subject_width)
+ pdf.Cell(g_width, 5, "", "LR")
+
+ pdf.SetY(top+1.5)
+
+ if i.is_a? Issue
+ i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
+ i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
+
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
+ i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
+
+ i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
+
+ i_left = ((i_start_date - gantt.date_from)*zoom)
+ i_width = ((i_end_date - i_start_date + 1)*zoom)
+ d_width = ((i_done_date - i_start_date)*zoom)
+ l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
+ l_width ||= 0
+
+ pdf.SetX(subject_width + i_left)
+ pdf.SetFillColor(200,200,200)
+ pdf.Cell(i_width, 2, "", 0, 0, "", 1)
+
+ if l_width > 0
+ pdf.SetY(top+1.5)
+ pdf.SetX(subject_width + i_left)
+ pdf.SetFillColor(255,100,100)
+ pdf.Cell(l_width, 2, "", 0, 0, "", 1)
+ end
+ if d_width > 0
+ pdf.SetY(top+1.5)
+ pdf.SetX(subject_width + i_left)
+ pdf.SetFillColor(100,100,255)
+ pdf.Cell(d_width, 2, "", 0, 0, "", 1)
+ end
+
+ pdf.SetY(top+1.5)
+ pdf.SetX(subject_width + i_left + i_width)
+ pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
+ else
+ i_left = ((i.start_date - gantt.date_from)*zoom)
+
+ pdf.SetX(subject_width + i_left)
+ pdf.SetFillColor(50,200,50)
+ pdf.Cell(2, 2, "", 0, 0, "", 1)
+
+ pdf.SetY(top+1.5)
+ pdf.SetX(subject_width + i_left + 3)
+ pdf.Cell(30, 2, "#{i.name}")
+ end
+
+ top = top + 5
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(15, top, subject_width+g_width, top)
+ if pdf.GetY() > 180
+ pdf.AddPage("L")
+ top = 20
+ pdf.Line(15, top, subject_width+g_width, top)
+ end
+ pdf.SetDrawColor(0, 0, 0)
+ end
+
+ pdf.Line(15, top, subject_width+g_width, top)
+ pdf.Output
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Helpers
+
+ # Simple class to compute the start and end dates of a calendar
+ class Calendar
+ include Redmine::I18n
+ attr_reader :startdt, :enddt
+
+ def initialize(date, lang = current_language, period = :month)
+ @date = date
+ @events = []
+ @ending_events_by_days = {}
+ @starting_events_by_days = {}
+ set_language_if_valid lang
+ case period
+ when :month
+ @startdt = Date.civil(date.year, date.month, 1)
+ @enddt = (@startdt >> 1)-1
+ # starts from the first day of the week
+ @startdt = @startdt - (@startdt.cwday - first_wday)%7
+ # ends on the last day of the week
+ @enddt = @enddt + (last_wday - @enddt.cwday)%7
+ when :week
+ @startdt = date - (date.cwday - first_wday)%7
+ @enddt = date + (last_wday - date.cwday)%7
+ else
+ raise 'Invalid period'
+ end
+ end
+
+ # Sets calendar events
+ def events=(events)
+ @events = events
+ @ending_events_by_days = @events.group_by {|event| event.due_date}
+ @starting_events_by_days = @events.group_by {|event| event.start_date}
+ end
+
+ # Returns events for the given day
+ def events_on(day)
+ ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
+ end
+
+ # Calendar current month
+ def month
+ @date.month
+ end
+
+ # Return the first day of week
+ # 1 = Monday ... 7 = Sunday
+ def first_wday
+ @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
+ end
+
+ def last_wday
+ @last_dow ||= (first_wday + 5)%7 + 1
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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
+ attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events
+
+ def initialize(options={})
+ options = options.dup
+ @events = []
+
+ 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
+ end
+
+ def events=(e)
+ @events = e.sort {|x,y| x.start_date <=> y.start_date }
+ end
+
+ def params
+ { :zoom => zoom, :year => year_from, :month => month_from, :months => months }
+ end
+
+ def params_previous
+ { :year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months }
+ end
+
+ def params_next
+ { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }
+ 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 = 320
+ header_heigth = 18
+ # width of one day in pixels
+ zoom = @zoom*2
+ g_width = (@date_to - @date_from + 1)*zoom
+ g_height = 20 * events.length + 20
+ headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
+ height = g_height + headers_heigth
+
+ imgl = Magick::ImageList.new
+ imgl.new_image(subject_width+g_width+1, height)
+ gc = Magick::Draw.new
+
+ # Subjects
+ top = headers_heigth + 20
+ gc.fill('black')
+ gc.stroke('transparent')
+ gc.stroke_width(1)
+ events.each do |i|
+ gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
+ top = top + 20
+ end
+
+ # 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_heigth
+ 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_heigth, left + width, 2*header_heigth + 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_heigth, left.round + width, 2*header_heigth + g_height-1)
+ gc.fill('black')
+ gc.stroke('transparent')
+ gc.stroke_width(1)
+ gc.text(left.round + 2, header_heigth + 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_heigth - 1
+ wday = @date_from.cwday
+ (date_to - @date_from + 1).to_i.times do
+ width = zoom
+ gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
+ gc.stroke('grey')
+ gc.stroke_width(1)
+ gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + 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_heigth)
+ gc.stroke('black')
+ gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
+
+ # content
+ top = headers_heigth + 20
+ gc.stroke('transparent')
+ events.each do |i|
+ if i.is_a?(Issue)
+ i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
+ i_end_date = (i.due_before <= date_to ? i.due_before : date_to )
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
+ i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
+ i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
+
+ i_left = subject_width + ((i_start_date - @date_from)*zoom).floor
+ i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
+ d_width = ((i_done_date - i_start_date)*zoom).floor # done width
+ l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
+
+ gc.fill('grey')
+ gc.rectangle(i_left, top, i_left + i_width, top - 6)
+ gc.fill('red')
+ gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
+ gc.fill('blue')
+ gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
+ gc.fill('black')
+ gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
+ else
+ i_left = subject_width + ((i.start_date - @date_from)*zoom).floor
+ gc.fill('green')
+ gc.rectangle(i_left, top, i_left + 6, top - 6)
+ gc.fill('black')
+ gc.text(i_left + 11, top + 1, i.name)
+ end
+ top = top + 20
+ end
+
+ # 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_heigth, x, headers_heigth + g_height-1)
+ end
+
+ gc.draw(imgl)
+ imgl.format = format
+ imgl.to_blob
+ end if Object.const_defined?(:Magick)
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Hook
+ include ActionController::UrlWriter
+
+ @@listener_classes = []
+ @@listeners = nil
+ @@hook_listeners = {}
+
+ class << self
+ # Adds a listener class.
+ # Automatically called when a class inherits from Redmine::Hook::Listener.
+ def add_listener(klass)
+ raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
+ @@listener_classes << klass
+ clear_listeners_instances
+ end
+
+ # Returns all the listerners instances.
+ def listeners
+ @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
+ end
+
+ # Returns the listeners instances for the given hook.
+ def hook_listeners(hook)
+ @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
+ end
+
+ # Clears all the listeners.
+ def clear_listeners
+ @@listener_classes = []
+ clear_listeners_instances
+ end
+
+ # Clears all the listeners instances.
+ def clear_listeners_instances
+ @@listeners = nil
+ @@hook_listeners = {}
+ end
+
+ # Calls a hook.
+ # Returns the listeners response.
+ def call_hook(hook, context={})
+ returning [] do |response|
+ hls = hook_listeners(hook)
+ if hls.any?
+ hls.each {|listener| response << listener.send(hook, context)}
+ end
+ end
+ end
+ end
+
+ # Base class for hook listeners.
+ class Listener
+ include Singleton
+ include Redmine::I18n
+
+ # Registers the listener
+ def self.inherited(child)
+ Redmine::Hook.add_listener(child)
+ super
+ end
+
+ end
+
+ # Listener class used for views hooks.
+ # Listeners that inherit this class will include various helpers by default.
+ class ViewListener < Listener
+ include ERB::Util
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::FormHelper
+ include ActionView::Helpers::FormTagHelper
+ include ActionView::Helpers::FormOptionsHelper
+ include ActionView::Helpers::JavaScriptHelper
+ include ActionView::Helpers::PrototypeHelper
+ include ActionView::Helpers::NumberHelper
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::TextHelper
+ include ActionController::UrlWriter
+ include ApplicationHelper
+
+ # Default to creating links using only the path. Subclasses can
+ # change this default as needed
+ def self.default_url_options
+ {:only_path => true }
+ end
+
+ # Helper method to directly render a partial using the context:
+ #
+ # class MyHook < Redmine::Hook::ViewListener
+ # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
+ # end
+ #
+ def self.render_on(hook, options={})
+ define_method hook do |context|
+ context[:controller].send(:render_to_string, {:locals => context}.merge(options))
+ end
+ end
+ end
+
+ # Helper module included in ApplicationHelper and ActionControllerso that
+ # hooks can be called in views like this:
+ #
+ # <%= call_hook(:some_hook) %>
+ # <%= call_hook(:another_hook, :foo => 'bar' %>
+ #
+ # Or in controllers like:
+ # call_hook(:some_hook)
+ # call_hook(:another_hook, :foo => 'bar'
+ #
+ # Hooks added to views will be concatenated into a string. Hooks added to
+ # controllers will return an array of results.
+ #
+ # Several objects are automatically added to the call context:
+ #
+ # * project => current project
+ # * request => Request instance
+ # * controller => current Controller instance
+ #
+ module Helper
+ def call_hook(hook, context={})
+ if is_a?(ActionController::Base)
+ default_context = {:controller => self, :project => @project, :request => request}
+ Redmine::Hook.call_hook(hook, default_context.merge(context))
+ else
+ default_context = {:controller => controller, :project => @project, :request => request}
+ Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
+ end
+ end
+ end
+ end
+end
+
+ApplicationHelper.send(:include, Redmine::Hook::Helper)
+ActionController::Base.send(:include, Redmine::Hook::Helper)
--- /dev/null
+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
+ Setting.date_format.blank? ? ::I18n.l(date.to_date) : date.strftime(Setting.date_format)
+ end
+
+ def format_time(time, include_date = true)
+ return nil unless time
+ 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)
+ Setting.time_format.blank? ? ::I18n.l(local, :format => (include_date ? :default : :time)) :
+ ((include_date ? "#{format_date(time)} " : "") + "#{local.strftime(Setting.time_format)}")
+ end
+
+ def day_name(day)
+ ::I18n.t('date.day_names')[day % 7]
+ end
+
+ def month_name(month)
+ ::I18n.t('date.month_names')[month]
+ end
+
+ def valid_languages
+ @@valid_languages ||= Dir.glob(File.join(RAILS_ROOT, 'config', 'locales', '*.yml')).collect {|f| File.basename(f).split('.').first}.collect(&:to_sym)
+ 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
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'net/imap'
+
+module Redmine
+ module IMAP
+ class << self
+ def check(imap_options={}, options={})
+ host = imap_options[:host] || '127.0.0.1'
+ port = imap_options[:port] || '143'
+ ssl = !imap_options[:ssl].nil?
+ folder = imap_options[:folder] || 'INBOX'
+
+ imap = Net::IMAP.new(host, port, ssl)
+ imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
+ imap.select(folder)
+ imap.search(['NOT', 'SEEN']).each do |message_id|
+ msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
+ logger.debug "Receiving message #{message_id}" if logger && logger.debug?
+ if MailHandler.receive(msg, options)
+ logger.debug "Message #{message_id} successfully received" if logger && logger.debug?
+ if imap_options[:move_on_success]
+ imap.copy(message_id, imap_options[:move_on_success])
+ end
+ imap.store(message_id, "+FLAGS", [:Seen, :Deleted])
+ else
+ logger.debug "Message #{message_id} can not be processed" if logger && logger.debug?
+ imap.store(message_id, "+FLAGS", [:Seen])
+ if imap_options[:move_on_failure]
+ imap.copy(message_id, imap_options[:move_on_failure])
+ imap.store(message_id, "+FLAGS", [:Deleted])
+ end
+ end
+ end
+ imap.expunge
+ end
+
+ private
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+ end
+ end
+end
--- /dev/null
+module Redmine
+ module Info
+ class << self
+ def app_name; 'Redmine' end
+ def url; 'http://www.redmine.org/' end
+ def help_url; 'http://www.redmine.org/guide' end
+ def versioned_name; "#{app_name} #{Redmine::VERSION}" end
+
+ # Creates the url string to a specific Redmine issue
+ def issue(issue_id)
+ url + 'issues/' + issue_id.to_s
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 MenuManager
+ module MenuController
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
+ mattr_accessor :menu_items
+
+ # Set the menu item name for a controller or specific actions
+ # Examples:
+ # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
+ # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
+ # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
+ #
+ # The default menu item name for a controller is controller_name by default
+ # Eg. the default menu item name for ProjectsController is :projects
+ def menu_item(id, options = {})
+ if actions = options[:only]
+ actions = [] << actions unless actions.is_a?(Array)
+ actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
+ else
+ menu_items[controller_name.to_sym][:default] = id
+ end
+ end
+ end
+
+ def menu_items
+ self.class.menu_items
+ end
+
+ # Returns the menu item name according to the current action
+ def current_menu_item
+ @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
+ menu_items[controller_name.to_sym][:default]
+ end
+
+ # Redirects user to the menu item of the given project
+ # Returns false if user is not authorized
+ def redirect_to_project_menu_item(project, name)
+ item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
+ if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
+ redirect_to({item.param => project}.merge(item.url))
+ return true
+ end
+ false
+ end
+ end
+
+ module MenuHelper
+ # Returns the current menu item name
+ def current_menu_item
+ @controller.current_menu_item
+ end
+
+ # Renders the application main menu
+ def render_main_menu(project)
+ render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
+ end
+
+ def render_menu(menu, project=nil)
+ links = []
+ menu_items_for(menu, project) do |item, caption, url, selected|
+ links << content_tag('li',
+ link_to(h(caption), url, item.html_options(:selected => selected)))
+ end
+ links.empty? ? nil : content_tag('ul', links.join("\n"))
+ end
+
+ def menu_items_for(menu, project=nil)
+ items = []
+ Redmine::MenuManager.allowed_items(menu, User.current, project).each do |item|
+ unless item.condition && !item.condition.call(project)
+ url = case item.url
+ when Hash
+ project.nil? ? item.url : {item.param => project}.merge(item.url)
+ when Symbol
+ send(item.url)
+ else
+ item.url
+ end
+ caption = item.caption(project)
+ if block_given?
+ yield item, caption, url, (current_menu_item == item.name)
+ else
+ items << [item, caption, url, (current_menu_item == item.name)]
+ end
+ end
+ end
+ return block_given? ? nil : items
+ end
+ end
+
+ class << self
+ def map(menu_name)
+ @items ||= {}
+ mapper = Mapper.new(menu_name.to_sym, @items)
+ if block_given?
+ yield mapper
+ else
+ mapper
+ end
+ end
+
+ def items(menu_name)
+ @items[menu_name.to_sym] || []
+ end
+
+ def allowed_items(menu_name, user, project)
+ project ? items(menu_name).select {|item| user && user.allowed_to?(item.url, project)} : items(menu_name)
+ end
+ end
+
+ class Mapper
+ def initialize(menu, items)
+ items[menu] ||= []
+ @menu = menu
+ @menu_items = items[menu]
+ end
+
+ @@last_items_count = Hash.new {|h,k| h[k] = 0}
+
+ # Adds an item at the end of the menu. Available options:
+ # * param: the parameter name that is used for the project id (default is :id)
+ # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
+ # * caption that can be:
+ # * a localized string Symbol
+ # * a String
+ # * a Proc that can take the project as argument
+ # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
+ # * last: menu item will stay at the end (eg. :last => true)
+ # * html_options: a hash of html options that are passed to link_to
+ def push(name, url, options={})
+ options = options.dup
+
+ # menu item position
+ if before = options.delete(:before)
+ position = @menu_items.collect(&:name).index(before)
+ elsif after = options.delete(:after)
+ position = @menu_items.collect(&:name).index(after)
+ position += 1 unless position.nil?
+ elsif options.delete(:last)
+ position = @menu_items.size
+ @@last_items_count[@menu] += 1
+ end
+ # default position
+ position ||= @menu_items.size - @@last_items_count[@menu]
+
+ @menu_items.insert(position, MenuItem.new(name, url, options))
+ end
+
+ # Removes a menu item
+ def delete(name)
+ @menu_items.delete_if {|i| i.name == name}
+ end
+ end
+
+ class MenuItem
+ include Redmine::I18n
+ attr_reader :name, :url, :param, :condition
+
+ def initialize(name, url, options)
+ raise "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
+ raise "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
+ @name = name
+ @url = url
+ @condition = options[:if]
+ @param = options[:param] || :id
+ @caption = options[:caption]
+ @html_options = options[:html] || {}
+ # Adds a unique class to each menu item based on its name
+ @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
+ end
+
+ def caption(project=nil)
+ if @caption.is_a?(Proc)
+ c = @caption.call(project).to_s
+ c = @name.to_s.humanize if c.blank?
+ c
+ else
+ if @caption.nil?
+ l_or_humanize(name, :prefix => 'label_')
+ else
+ @caption.is_a?(Symbol) ? l(@caption) : @caption
+ end
+ end
+ end
+
+ def html_options(options={})
+ if options[:selected]
+ o = @html_options.dup
+ o[:class] += ' selected'
+ o
+ else
+ @html_options
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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-javascript' => 'js',
+ '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',
+ 'image/gif' => 'gif',
+ 'image/jpeg' => 'jpg,jpeg,jpe',
+ 'image/png' => 'png',
+ 'image/tiff' => 'tiff,tif',
+ 'image/x-ms-bmp' => 'bmp',
+ 'image/x-xpixmap' => 'xpm',
+ 'application/pdf' => 'pdf',
+ '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
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Platform
+ class << self
+ def mswin?
+ (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 <tt>register</tt> 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:
+ # * <tt>:default</tt>: default value for the plugin settings
+ # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
+ # Example:
+ # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
+ # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
+ #
+ # When rendered, the plugin settings value is available as the local variable +settings+
+ class Plugin
+ @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 <plugin>/config/locales/
+ ::I18n.load_path += Dir.glob(File.join(RAILS_ROOT, 'vendor', 'plugins', id.to_s, 'config', 'locales', '*.yml'))
+ registered_plugins[id] = p
+ end
+
+ # Returns an array off 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
+
+ def initialize(id)
+ @id = id.to_sym
+ 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 a specific Redmine version
+ # requires_redmine :version => '0.7.3' # 0.7.3 only
+ # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
+ 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, 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 Redmine #{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 Redmine versions: #{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 can be used to make the permission public (implicitly given to any user)
+ # or to restrict users the permission can be given to.
+ #
+ # 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
+ #
+ # # 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:
+ # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
+ # * <tt>:default</tt> - 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
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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'
+
+module Redmine
+ module Scm
+ module Adapters
+ class CommandFailed < StandardError #:nodoc:
+ end
+
+ class AbstractAdapter #:nodoc:
+ class << self
+ # 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
+ end
+
+ def initialize(url, root_url=nil, login=nil, password=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
+
+ # 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)
+ 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)
+ if Redmine::Platform.mswin?
+ '"' + str.gsub(/"/, '\\"') + '"'
+ else
+ "'" + str.gsub(/'/, "'\"'\"'") + "'"
+ end
+ end
+
+ private
+ def retrieve_root_url
+ info = self.info
+ info ? info.root_url : nil
+ end
+
+ def target(path)
+ path ||= ''
+ base = path.match(/^\//) ? root_url : url
+ shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
+ end
+
+ def logger
+ self.class.logger
+ end
+
+ def shellout(cmd, &block)
+ self.class.shellout(cmd, &block)
+ end
+
+ def self.logger
+ RAILS_DEFAULT_LOGGER
+ end
+
+ def self.shellout(cmd, &block)
+ logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
+ if Rails.env == 'development'
+ # Capture stderr when running in dev environment
+ cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
+ end
+ begin
+ IO.popen(cmd, "r+") do |io|
+ io.close_write
+ block.call(io) if block_given?
+ end
+ rescue Errno::ENOENT => e
+ msg = strip_credential(e.message)
+ # The command failed, log it and re-raise
+ logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
+ 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
+ end
+
+ class Entries < Array
+ def sort_by_name
+ 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
+ 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
+ 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 :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
+
+ 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]
+ end
+
+ def save(repo)
+ if repo.changesets.find_by_scmid(scmid.to_s).nil?
+ changeset = Changeset.create!(
+ :repository => repo,
+ :revision => identifier,
+ :scmid => scmid,
+ :committer => author,
+ :committed_on => time,
+ :comments => message)
+
+ paths.each do |file|
+ Change.create!(
+ :changeset => changeset,
+ :action => file[:action],
+ :path => file[:path])
+ end
+ 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
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class BazaarAdapter < AbstractAdapter
+
+ # Bazaar executable name
+ BZR_BIN = "bzr"
+
+ # Get info about the repository
+ def info
+ cmd = "#{BZR_BIN} revno #{target('')}"
+ info = nil
+ shellout(cmd) do |io|
+ if io.read =~ %r{^(\d+)$}
+ info = Info.new({:root_url => url,
+ :lastrev => Revision.new({
+ :identifier => $1
+ })
+ })
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ info
+ rescue CommandFailed
+ return nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ entries = Entries.new
+ cmd = "#{BZR_BIN} ls -v --show-ids"
+ identifier = -1 unless identifier && identifier.to_i > 0
+ cmd << " -r#{identifier.to_i}"
+ cmd << " #{target(path)}"
+ shellout(cmd) do |io|
+ prefix = "#{url}/#{path}".gsub('\\', '/')
+ logger.debug "PREFIX: #{prefix}"
+ re = %r{^V\s+#{Regexp.escape(prefix)}(\/?)([^\/]+)(\/?)\s+(\S+)$}
+ io.each_line do |line|
+ next unless line =~ re
+ entries << Entry.new({:name => $2.strip,
+ :path => ((path.empty? ? "" : "#{path}/") + $2.strip),
+ :kind => ($3.blank? ? 'file' : 'dir'),
+ :size => nil,
+ :lastrev => Revision.new(:revision => $4.strip)
+ })
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
+ entries.sort_by_name
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ path ||= ''
+ identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0
+ identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
+ revisions = Revisions.new
+ cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}"
+ shellout(cmd) do |io|
+ revision = nil
+ parsing = nil
+ io.each_line do |line|
+ if line =~ /^----/
+ revisions << revision if revision
+ revision = Revision.new(:paths => [], :message => '')
+ parsing = nil
+ else
+ next unless revision
+
+ if line =~ /^revno: (\d+)($|\s\[merge\]$)/
+ revision.identifier = $1.to_i
+ elsif line =~ /^committer: (.+)$/
+ revision.author = $1.strip
+ elsif line =~ /^revision-id:(.+)$/
+ revision.scmid = $1.strip
+ elsif line =~ /^timestamp: (.+)$/
+ revision.time = Time.parse($1).localtime
+ elsif line =~ /^ -----/
+ # partial revisions
+ parsing = nil unless parsing == 'message'
+ elsif line =~ /^(message|added|modified|removed|renamed):/
+ parsing = $1
+ elsif line =~ /^ (.*)$/
+ if parsing == 'message'
+ revision.message << "#{$1}\n"
+ else
+ if $1 =~ /^(.*)\s+(\S+)$/
+ path = $1.strip
+ revid = $2
+ case parsing
+ when 'added'
+ revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
+ when 'modified'
+ revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
+ when 'removed'
+ revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
+ when 'renamed'
+ new_path = path.split('=>').last
+ revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
+ end
+ end
+ end
+ else
+ parsing = nil
+ end
+ end
+ end
+ revisions << revision if revision
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ path ||= ''
+ if identifier_to
+ identifier_to = identifier_to.to_i
+ else
+ identifier_to = identifier_from.to_i - 1
+ end
+ cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ #return nil if $? && $?.exitstatus != 0
+ diff
+ end
+
+ def cat(path, identifier=nil)
+ cmd = "#{BZR_BIN} cat"
+ cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
+ cmd << " #{target(path)}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+
+ def annotate(path, identifier=nil)
+ cmd = "#{BZR_BIN} annotate --all"
+ cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
+ cmd << " #{target(path)}"
+ blame = Annotate.new
+ shellout(cmd) do |io|
+ author = nil
+ identifier = nil
+ io.each_line do |line|
+ next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
+ blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ blame
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class CvsAdapter < AbstractAdapter
+
+ # CVS executable name
+ CVS_BIN = "cvs"
+
+ # Guidelines for the input:
+ # url -> the project-path, relative to the cvsroot (eg. module name)
+ # root_url -> the good old, sometimes damned, CVSROOT
+ # login -> unnecessary
+ # password -> unnecessary too
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = url
+ @login = login if login && !login.empty?
+ @password = (password || "") if @login
+ #TODO: better Exception here (IllegalArgumentException)
+ raise CommandFailed if root_url.blank?
+ @root_url = root_url
+ end
+
+ def root_url
+ @root_url
+ end
+
+ def url
+ @url
+ end
+
+ def info
+ logger.debug "<cvs> info"
+ Info.new({:root_url => @root_url, :lastrev => nil})
+ end
+
+ def get_previous_revision(revision)
+ CvsRevisionHelper.new(revision).prevRev
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ # this method is used by the repository-browser (aka LIST)
+ def entries(path=nil, identifier=nil)
+ logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ entries = Entries.new
+ cmd = "#{CVS_BIN} -d #{root_url} rls -e"
+ cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
+ cmd << " #{shell_quote path_with_project}"
+ shellout(cmd) do |io|
+ io.each_line(){|line|
+ fields=line.chop.split('/',-1)
+ logger.debug(">>InspectLine #{fields.inspect}")
+
+ if fields[0]!="D"
+ entries << Entry.new({:name => fields[-5],
+ #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
+ :path => "#{path}/#{fields[-5]}",
+ :kind => 'file',
+ :size => nil,
+ :lastrev => Revision.new({
+ :revision => fields[-4],
+ :name => fields[-4],
+ :time => Time.parse(fields[-3]),
+ :author => ''
+ })
+ })
+ else
+ entries << Entry.new({:name => fields[1],
+ :path => "#{path}/#{fields[1]}",
+ :kind => 'dir',
+ :size => nil,
+ :lastrev => nil
+ })
+ end
+ }
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ end
+
+ STARTLOG="----------------------------"
+ ENDLOG ="============================================================================="
+
+ # Returns all revisions found between identifier_from and identifier_to
+ # in the repository. both identifier have to be dates or nil.
+ # these method returns nothing but yield every result in block
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
+ logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
+
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} rlog"
+ cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
+ cmd << " #{shell_quote path_with_project}"
+ shellout(cmd) do |io|
+ state="entry_start"
+
+ commit_log=String.new
+ revision=nil
+ date=nil
+ author=nil
+ entry_path=nil
+ entry_name=nil
+ file_state=nil
+ branch_map=nil
+
+ io.each_line() do |line|
+
+ if state!="revision" && /^#{ENDLOG}/ =~ line
+ commit_log=String.new
+ revision=nil
+ state="entry_start"
+ end
+
+ if state=="entry_start"
+ branch_map=Hash.new
+ if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
+ entry_path = normalize_cvs_path($1)
+ entry_name = normalize_path(File.basename($1))
+ logger.debug("Path #{entry_path} <=> Name #{entry_name}")
+ elsif /^head: (.+)$/ =~ line
+ entry_headRev = $1 #unless entry.nil?
+ elsif /^symbolic names:/ =~ line
+ state="symbolic" #unless entry.nil?
+ elsif /^#{STARTLOG}/ =~ line
+ commit_log=String.new
+ state="revision"
+ end
+ next
+ elsif state=="symbolic"
+ if /^(.*):\s(.*)/ =~ (line.strip)
+ branch_map[$1]=$2
+ else
+ state="tags"
+ next
+ end
+ elsif state=="tags"
+ if /^#{STARTLOG}/ =~ line
+ commit_log = ""
+ state="revision"
+ elsif /^#{ENDLOG}/ =~ line
+ state="head"
+ end
+ next
+ elsif state=="revision"
+ if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
+ if revision
+
+ revHelper=CvsRevisionHelper.new(revision)
+ revBranch="HEAD"
+
+ branch_map.each() do |branch_name,branch_point|
+ if revHelper.is_in_branch_with_symbol(branch_point)
+ revBranch=branch_name
+ end
+ end
+
+ logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
+
+ yield Revision.new({
+ :time => date,
+ :author => author,
+ :message=>commit_log.chomp,
+ :paths => [{
+ :revision => revision,
+ :branch=> revBranch,
+ :path=>entry_path,
+ :name=>entry_name,
+ :kind=>'file',
+ :action=>file_state
+ }]
+ })
+ end
+
+ commit_log=String.new
+ revision=nil
+
+ if /^#{ENDLOG}/ =~ line
+ state="entry_start"
+ end
+ next
+ end
+
+ if /^branches: (.+)$/ =~ line
+ #TODO: version.branch = $1
+ elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
+ revision = $1
+ elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
+ date = Time.parse($1)
+ author = /author: ([^;]+)/.match(line)[1]
+ file_state = /state: ([^;]+)/.match(line)[1]
+ #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
+ # useful for stats or something else
+ # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
+ # unless linechanges.nil?
+ # version.line_plus = linechanges[1]
+ # version.line_minus = linechanges[2]
+ # else
+ # version.line_plus = 0
+ # version.line_minus = 0
+ # end
+ else
+ commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
+ end
+ end
+ end
+ end
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ diff
+ end
+
+ def cat(path, identifier=nil)
+ identifier = (identifier) ? identifier : "HEAD"
+ logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} co"
+ cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
+ cmd << " -p #{shell_quote path_with_project}"
+ cat = nil
+ shellout(cmd) do |io|
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+
+ def annotate(path, identifier=nil)
+ identifier = (identifier) ? identifier : "HEAD"
+ logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
+ blame = Annotate.new
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
+ blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ blame
+ end
+
+ private
+
+ # Returns the root url without the connexion string
+ # :pserver:anonymous@foo.bar:/path => /path
+ # :ext:cvsservername:/path => /path
+ def root_url_path
+ root_url.to_s.gsub(/^:.+:\d*/, '')
+ end
+
+ # convert a date/time into the CVS-format
+ def time_to_cvstime(time)
+ return nil if time.nil?
+ unless time.kind_of? Time
+ time = Time.parse(time)
+ end
+ return time.strftime("%Y-%m-%d %H:%M:%S")
+ end
+
+ def normalize_cvs_path(path)
+ normalize_path(path.gsub(/Attic\//,''))
+ end
+
+ def normalize_path(path)
+ path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
+ end
+ end
+
+ class CvsRevisionHelper
+ attr_accessor :complete_rev, :revision, :base, :branchid
+
+ def initialize(complete_rev)
+ @complete_rev = complete_rev
+ parseRevision()
+ end
+
+ def branchPoint
+ return @base
+ end
+
+ def branchVersion
+ if isBranchRevision
+ return @base+"."+@branchid
+ end
+ return @base
+ end
+
+ def isBranchRevision
+ !@branchid.nil?
+ end
+
+ def prevRev
+ unless @revision==0
+ return buildRevision(@revision-1)
+ end
+ return buildRevision(@revision)
+ end
+
+ def is_in_branch_with_symbol(branch_symbol)
+ bpieces=branch_symbol.split(".")
+ branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
+ return (branchVersion==branch_start)
+ end
+
+ private
+ def buildRevision(rev)
+ if rev== 0
+ @base
+ elsif @branchid.nil?
+ @base+"."+rev.to_s
+ else
+ @base+"."+@branchid+"."+rev.to_s
+ end
+ end
+
+ # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
+ def parseRevision()
+ pieces=@complete_rev.split(".")
+ @revision=pieces.last.to_i
+ baseSize=1
+ baseSize+=(pieces.size/2)
+ @base=pieces[0..-baseSize].join(".")
+ if baseSize > 2
+ @branchid=pieces[-2]
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+require 'rexml/document'
+
+module Redmine
+ module Scm
+ module Adapters
+ class DarcsAdapter < AbstractAdapter
+ # Darcs executable name
+ DARCS_BIN = "darcs"
+
+ class << self
+ def client_version
+ @@client_version ||= (darcs_binary_version || [])
+ end
+
+ def darcs_binary_version
+ cmd = "#{DARCS_BIN} --version"
+ version = nil
+ shellout(cmd) do |io|
+ # Read darcs version in first returned line
+ if m = io.gets.match(%r{((\d+\.)+\d+)})
+ version = m[0].scan(%r{\d+}).collect(&:to_i)
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ version
+ end
+ end
+
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = url
+ @root_url = url
+ end
+
+ def supports_cat?
+ # cat supported in darcs 2.0.0 and higher
+ self.class.client_version_above?([2, 0, 0])
+ end
+
+ # Get info about the darcs repository
+ def info
+ rev = revisions(nil,nil,nil,{:limit => 1})
+ rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ def entries(path=nil, identifier=nil)
+ path_prefix = (path.blank? ? '' : "#{path}/")
+ path = '.' if path.blank?
+ entries = Entries.new
+ cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
+ cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
+ cmd << " #{shell_quote path}"
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ if doc.root.name == 'directory'
+ doc.elements.each('directory/*') do |element|
+ next unless ['file', 'directory'].include? element.name
+ entries << entry_from_xml(element, path_prefix)
+ end
+ elsif doc.root.name == 'file'
+ entries << entry_from_xml(doc.root, path_prefix)
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.compact.sort_by_name
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ path = '.' if path.blank?
+ revisions = Revisions.new
+ cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
+ cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
+ cmd << " --last #{options[:limit].to_i}" if options[:limit]
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("changelog/patch") do |patch|
+ message = patch.elements['name'].text
+ message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
+ revisions << Revision.new({:identifier => nil,
+ :author => patch.attributes['author'],
+ :scmid => patch.attributes['hash'],
+ :time => Time.parse(patch.attributes['local_date']),
+ :message => message,
+ :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
+ })
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ path = '*' if path.blank?
+ cmd = "#{DARCS_BIN} diff --repodir #{@url}"
+ if identifier_to.nil?
+ cmd << " --match #{shell_quote("hash #{identifier_from}")}"
+ else
+ cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
+ cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
+ end
+ cmd << " -u #{shell_quote path}"
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ diff
+ end
+
+ def cat(path, identifier=nil)
+ cmd = "#{DARCS_BIN} show content --repodir #{@url}"
+ cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
+ cmd << " #{shell_quote path}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+
+ private
+
+ # Returns an Entry from the given XML element
+ # or nil if the entry was deleted
+ def entry_from_xml(element, path_prefix)
+ modified_element = element.elements['modified']
+ if modified_element.elements['modified_how'].text.match(/removed/)
+ return nil
+ end
+
+ Entry.new({:name => element.attributes['name'],
+ :path => path_prefix + element.attributes['name'],
+ :kind => element.name == 'file' ? 'file' : 'dir',
+ :size => nil,
+ :lastrev => Revision.new({
+ :identifier => nil,
+ :scmid => modified_element.elements['patch'].attributes['hash']
+ })
+ })
+ end
+
+ # Retrieve changed paths for a single patch
+ def get_paths_for_patch(hash)
+ cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
+ cmd << " --match #{shell_quote("hash #{hash}")} "
+ paths = []
+ shellout(cmd) do |io|
+ begin
+ # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
+ # A root element is added so that REXML doesn't raise an error
+ doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
+ doc.elements.each('fake_root/summary/*') do |modif|
+ paths << {:action => modif.name[0,1].upcase,
+ :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
+ }
+ end
+ rescue
+ end
+ end
+ paths
+ rescue CommandFailed
+ paths
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# FileSystem adapter
+# File written by Paul Rivier, at Demotera.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+require 'find'
+
+module Redmine
+ module Scm
+ module Adapters
+ class FilesystemAdapter < AbstractAdapter
+
+
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = with_trailling_slash(url)
+ end
+
+ def format_path_ends(path, leading=true, trailling=true)
+ path = leading ? with_leading_slash(path) :
+ without_leading_slash(path)
+ trailling ? with_trailling_slash(path) :
+ without_trailling_slash(path)
+ end
+
+ def info
+ info = Info.new({:root_url => target(),
+ :lastrev => nil
+ })
+ info
+ rescue CommandFailed
+ return nil
+ end
+
+ def entries(path="", identifier=nil)
+ entries = Entries.new
+ Dir.new(target(path)).each do |e|
+ relative_path = format_path_ends((format_path_ends(path,
+ false,
+ true) + e),
+ false,false)
+ target = target(relative_path)
+ entries <<
+ Entry.new({ :name => File.basename(e),
+ # below : list unreadable files, but dont link them.
+ :path => File.readable?(target) ? relative_path : "",
+ :kind => (File.directory?(target) ? 'dir' : 'file'),
+ :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
+ :lastrev =>
+ Revision.new({:time => (File.mtime(target)).localtime,
+ })
+ }) if File.exist?(target) and # paranoid test
+ %w{file directory}.include?(File.ftype(target)) and # avoid special types
+ not File.basename(e).match(/^\.+$/) # avoid . and ..
+ end
+ entries.sort_by_name
+ end
+
+ def cat(path, identifier=nil)
+ File.new(target(path), "rb").read
+ end
+
+ private
+
+ # AbstractAdapter::target is implicitly made to quote paths.
+ # Here we do not shell-out, so we do not want quotes.
+ def target(path=nil)
+ #Prevent the use of ..
+ if path and !path.match(/(^|\/)\.\.(\/|$)/)
+ return "#{self.url}#{without_leading_slash(path)}"
+ end
+ return self.url
+ end
+
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class GitAdapter < AbstractAdapter
+ # Git executable name
+ GIT_BIN = "git"
+
+ def info
+ begin
+ Info.new(:root_url => url, :lastrev => lastrev('',nil))
+ rescue
+ nil
+ end
+ end
+
+ def branches
+ branches = []
+ cmd = "#{GIT_BIN} --git-dir #{target('')} branch"
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ branches << line.match('\s*\*?\s*(.*)$')[1]
+ end
+ end
+ branches.sort!
+ end
+
+ def tags
+ tags = []
+ cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
+ shellout(cmd) do |io|
+ io.readlines.sort!.map{|t| t.strip}
+ end
+ end
+
+ def default_branch
+ branches.include?('master') ? 'master' : branches.first
+ end
+
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ entries = Entries.new
+ cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
+ cmd << shell_quote("HEAD:" + path) if identifier.nil?
+ cmd << shell_quote(identifier + ":" + path) if identifier
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ e = line.chomp.to_s
+ if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
+ type = $1
+ sha = $2
+ size = $3
+ name = $4
+ full_path = path.empty? ? name : "#{path}/#{name}"
+ entries << Entry.new({:name => name,
+ :path => full_path,
+ :kind => (type == "tree") ? 'dir' : 'file',
+ :size => (type == "tree") ? nil : size,
+ :lastrev => lastrev(full_path,identifier)
+ }) unless entries.detect{|entry| entry.name == name}
+ end
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ end
+
+ def lastrev(path,rev)
+ return nil if path.nil?
+ cmd = "#{GIT_BIN} --git-dir #{target('')} log --pretty=fuller --no-merges -n 1 "
+ cmd << " #{shell_quote rev} " if rev
+ cmd << "-- #{path} " unless path.empty?
+ shellout(cmd) do |io|
+ begin
+ id = io.gets.split[1]
+ author = io.gets.match('Author:\s+(.*)$')[1]
+ 2.times { io.gets }
+ time = io.gets.match('CommitDate:\s+(.*)$')[1]
+
+ Revision.new({
+ :identifier => id,
+ :scmid => id,
+ :author => author,
+ :time => time,
+ :message => nil,
+ :paths => nil
+ })
+ rescue NoMethodError => e
+ logger.error("The revision '#{path}' has a wrong format")
+ return nil
+ end
+ end
+ end
+
+ def num_revisions
+ cmd = "#{GIT_BIN} --git-dir #{target('')} log --all --pretty=format:'' | wc -l"
+ shellout(cmd) {|io| io.gets.chomp.to_i + 1}
+ end
+
+ def revisions(path, identifier_from, identifier_to, options={})
+ revisions = Revisions.new
+
+ cmd = "#{GIT_BIN} --git-dir #{target('')} log --find-copies-harder --raw --date=iso --pretty=fuller"
+ cmd << " --reverse" if options[:reverse]
+ cmd << " --all" if options[:all]
+ cmd << " -n #{options[:limit]} " if options[:limit]
+ cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
+ cmd << " #{shell_quote identifier_to} " if identifier_to
+ cmd << " -- #{path}" if path && !path.empty?
+
+ shellout(cmd) do |io|
+ files=[]
+ changeset = {}
+ parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
+ revno = 1
+
+ io.each_line do |line|
+ if line =~ /^commit ([0-9a-f]{40})$/
+ key = "commit"
+ value = $1
+ if (parsing_descr == 1 || parsing_descr == 2)
+ parsing_descr = 0
+ revision = Revision.new({
+ :identifier => changeset[:commit],
+ :scmid => changeset[:commit],
+ :author => changeset[:author],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => files
+ })
+ if block_given?
+ yield revision
+ else
+ revisions << revision
+ end
+ changeset = {}
+ files = []
+ revno = revno + 1
+ end
+ changeset[:commit] = $1
+ elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
+ key = $1
+ value = $2
+ if key == "Author"
+ changeset[:author] = value
+ elsif key == "CommitDate"
+ changeset[:date] = value
+ end
+ elsif (parsing_descr == 0) && line.chomp.to_s == ""
+ parsing_descr = 1
+ changeset[:description] = ""
+ elsif (parsing_descr == 1 || parsing_descr == 2) \
+ && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
+ parsing_descr = 2
+ fileaction = $1
+ filepath = $2
+ files << {:action => fileaction, :path => filepath}
+ elsif (parsing_descr == 1 || parsing_descr == 2) \
+ && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\s+(.+)$/
+ parsing_descr = 2
+ fileaction = $1
+ filepath = $3
+ files << {:action => fileaction, :path => filepath}
+ elsif (parsing_descr == 1) && line.chomp.to_s == ""
+ parsing_descr = 2
+ elsif (parsing_descr == 1)
+ changeset[:description] << line[4..-1]
+ end
+ end
+
+ if changeset[:commit]
+ revision = Revision.new({
+ :identifier => changeset[:commit],
+ :scmid => changeset[:commit],
+ :author => changeset[:author],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => files
+ })
+
+ if block_given?
+ yield revision
+ else
+ revisions << revision
+ end
+ end
+ end
+
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ path ||= ''
+
+ if identifier_to
+ cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}"
+ else
+ cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}"
+ end
+
+ cmd << " -- #{shell_quote path}" unless path.empty?
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ diff
+ end
+
+ def annotate(path, identifier=nil)
+ identifier = 'HEAD' if identifier.blank?
+ cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
+ blame = Annotate.new
+ content = nil
+ shellout(cmd) { |io| io.binmode; content = io.read }
+ return nil if $? && $?.exitstatus != 0
+ # git annotates binary files
+ return nil if content.is_binary_data?
+ content.split("\n").each do |line|
+ next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
+ blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
+ end
+ blame
+ end
+
+ def cat(path, identifier=nil)
+ if identifier.nil?
+ identifier = 'HEAD'
+ end
+ cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+ end
+ end
+ end
+end
--- /dev/null
+changeset = 'This template must be used with --debug option\n'
+changeset_quiet = 'This template must be used with --debug option\n'
+changeset_verbose = 'This template must be used with --debug option\n'
+changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file = '<path action="M">{file|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
\ No newline at end of file
--- /dev/null
+changeset = 'This template must be used with --debug option\n'
+changeset_quiet = 'This template must be used with --debug option\n'
+changeset_verbose = 'This template must be used with --debug option\n'
+changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file_mod = '<path action="M">{file_mod|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class MercurialAdapter < AbstractAdapter
+
+ # Mercurial executable name
+ HG_BIN = "hg"
+ TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
+ TEMPLATE_NAME = "hg-template"
+ TEMPLATE_EXTENSION = "tmpl"
+
+ class << self
+ def client_version
+ @@client_version ||= (hgversion || [])
+ end
+
+ def hgversion
+ # The hg version is expressed either as a
+ # release number (eg 0.9.5 or 1.0) or as a revision
+ # id composed of 12 hexa characters.
+ theversion = hgversion_from_command_line
+ if theversion.match(/^\d+(\.\d+)+/)
+ theversion.split(".").collect(&:to_i)
+ end
+ end
+
+ def hgversion_from_command_line
+ %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
+ end
+
+ def template_path
+ @@template_path ||= template_path_for(client_version)
+ end
+
+ def template_path_for(version)
+ if ((version <=> [0,9,5]) > 0) || version.empty?
+ ver = "1.0"
+ else
+ ver = "0.9.5"
+ end
+ "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
+ end
+ end
+
+ def info
+ cmd = "#{HG_BIN} -R #{target('')} root"
+ root_url = nil
+ shellout(cmd) do |io|
+ root_url = io.gets
+ end
+ return nil if $? && $?.exitstatus != 0
+ info = Info.new({:root_url => root_url.chomp,
+ :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
+ })
+ info
+ rescue CommandFailed
+ return nil
+ end
+
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ entries = Entries.new
+ cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
+ cmd << " " + shell_quote("path:#{path}") unless path.empty?
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ # HG uses antislashs as separator on Windows
+ line = line.gsub(/\\/, "/")
+ if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
+ e ||= line
+ e = e.chomp.split(%r{[\/\\]})
+ entries << Entry.new({:name => e.first,
+ :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
+ :kind => (e.size > 1 ? 'dir' : 'file'),
+ :lastrev => Revision.new
+ }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
+ end
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ end
+
+ # Fetch the revisions by using a template file that
+ # makes Mercurial produce a xml output.
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ revisions = Revisions.new
+ cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
+ if identifier_from && identifier_to
+ cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
+ elsif identifier_from
+ cmd << " -r #{identifier_from.to_i}:"
+ end
+ cmd << " --limit #{options[:limit].to_i}" if options[:limit]
+ cmd << " #{path}" if path
+ shellout(cmd) do |io|
+ begin
+ # HG doesn't close the XML Document...
+ doc = REXML::Document.new(io.read << "</log>")
+ doc.elements.each("log/logentry") do |logentry|
+ paths = []
+ copies = logentry.get_elements('paths/path-copied')
+ logentry.elements.each("paths/path") do |path|
+ # Detect if the added file is a copy
+ if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
+ from_path = c.attributes['copyfrom-path']
+ from_rev = logentry.attributes['revision']
+ end
+ paths << {:action => path.attributes['action'],
+ :path => "/#{path.text}",
+ :from_path => from_path ? "/#{from_path}" : nil,
+ :from_revision => from_rev ? from_rev : nil
+ }
+ end
+ paths.sort! { |x,y| x[:path] <=> y[:path] }
+
+ revisions << Revision.new({:identifier => logentry.attributes['revision'],
+ :scmid => logentry.attributes['node'],
+ :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
+ :time => Time.parse(logentry.elements['date'].text).localtime,
+ :message => logentry.elements['msg'].text,
+ :paths => paths
+ })
+ end
+ rescue
+ logger.debug($!)
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ path ||= ''
+ if identifier_to
+ identifier_to = identifier_to.to_i
+ else
+ identifier_to = identifier_from.to_i - 1
+ end
+ cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
+ cmd << " -I #{target(path)}" unless path.empty?
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ diff
+ end
+
+ def cat(path, identifier=nil)
+ cmd = "#{HG_BIN} -R #{target('')} cat"
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
+ cmd << " #{target(path)}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+
+ def annotate(path, identifier=nil)
+ path ||= ''
+ cmd = "#{HG_BIN} -R #{target('')}"
+ cmd << " annotate -n -u"
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
+ cmd << " -r #{identifier.to_i}" if identifier
+ cmd << " #{target(path)}"
+ blame = Annotate.new
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
+ blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ blame
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006-2007 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+require 'redmine/scm/adapters/abstract_adapter'\r
+require 'rexml/document'\r
+require 'uri'\r
+\r
+module Redmine\r
+ module Scm\r
+ module Adapters \r
+ class SubversionAdapter < AbstractAdapter\r
+ \r
+ # SVN executable name\r
+ SVN_BIN = "svn"\r
+ \r
+ class << self\r
+ def client_version\r
+ @@client_version ||= (svn_binary_version || [])\r
+ end\r
+ \r
+ def svn_binary_version\r
+ cmd = "#{SVN_BIN} --version"\r
+ version = nil\r
+ shellout(cmd) do |io|\r
+ # Read svn version in first returned line\r
+ if m = io.gets.to_s.match(%r{((\d+\.)+\d+)})\r
+ version = m[0].scan(%r{\d+}).collect(&:to_i)\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ version\r
+ end\r
+ end\r
+ \r
+ # Get info about the svn repository\r
+ def info\r
+ cmd = "#{SVN_BIN} info --xml #{target('')}"\r
+ cmd << credentials_string\r
+ info = nil\r
+ shellout(cmd) do |io|\r
+ begin\r
+ doc = REXML::Document.new(io)\r
+ #root_url = doc.elements["info/entry/repository/root"].text \r
+ info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,\r
+ :lastrev => Revision.new({\r
+ :identifier => doc.elements["info/entry/commit"].attributes['revision'],\r
+ :time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime,\r
+ :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")\r
+ })\r
+ })\r
+ rescue\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ info\r
+ rescue CommandFailed\r
+ return nil\r
+ end\r
+ \r
+ # Returns an Entries collection\r
+ # or nil if the given path doesn't exist in the repository\r
+ def entries(path=nil, identifier=nil)\r
+ path ||= ''\r
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+ entries = Entries.new\r
+ cmd = "#{SVN_BIN} list --xml #{target(URI.escape(path))}@#{identifier}"\r
+ cmd << credentials_string\r
+ shellout(cmd) do |io|\r
+ output = io.read\r
+ begin\r
+ doc = REXML::Document.new(output)\r
+ doc.elements.each("lists/list/entry") do |entry|\r
+ commit = entry.elements['commit']\r
+ commit_date = commit.elements['date']\r
+ # Skip directory if there is no commit date (usually that\r
+ # means that we don't have read access to it)\r
+ next if entry.attributes['kind'] == 'dir' && commit_date.nil?\r
+ name = entry.elements['name'].text\r
+ entries << Entry.new({:name => URI.unescape(name),\r
+ :path => ((path.empty? ? "" : "#{path}/") + name),\r
+ :kind => entry.attributes['kind'],\r
+ :size => ((s = entry.elements['size']) ? s.text.to_i : nil),\r
+ :lastrev => Revision.new({\r
+ :identifier => commit.attributes['revision'],\r
+ :time => Time.parse(commit_date.text).localtime,\r
+ :author => ((a = commit.elements['author']) ? a.text : nil)\r
+ })\r
+ })\r
+ end\r
+ rescue Exception => e\r
+ logger.error("Error parsing svn output: #{e.message}")\r
+ logger.error("Output was:\n #{output}")\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?\r
+ entries.sort_by_name\r
+ end\r
+ \r
+ def properties(path, identifier=nil)\r
+ # proplist xml output supported in svn 1.5.0 and higher\r
+ return nil unless self.class.client_version_above?([1, 5, 0])\r
+ \r
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+ cmd = "#{SVN_BIN} proplist --verbose --xml #{target(URI.escape(path))}@#{identifier}"\r
+ cmd << credentials_string\r
+ properties = {}\r
+ shellout(cmd) do |io|\r
+ output = io.read\r
+ begin\r
+ doc = REXML::Document.new(output)\r
+ doc.elements.each("properties/target/property") do |property|\r
+ properties[ property.attributes['name'] ] = property.text\r
+ end\r
+ rescue\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ properties\r
+ end\r
+ \r
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})\r
+ path ||= ''\r
+ identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"\r
+ identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1\r
+ revisions = Revisions.new\r
+ cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"\r
+ cmd << credentials_string\r
+ cmd << " --verbose " if options[:with_paths]\r
+ cmd << " --limit #{options[:limit].to_i}" if options[:limit]\r
+ cmd << ' ' + target(URI.escape(path))\r
+ shellout(cmd) do |io|\r
+ begin\r
+ doc = REXML::Document.new(io)\r
+ doc.elements.each("log/logentry") do |logentry|\r
+ paths = []\r
+ logentry.elements.each("paths/path") do |path|\r
+ paths << {:action => path.attributes['action'],\r
+ :path => path.text,\r
+ :from_path => path.attributes['copyfrom-path'],\r
+ :from_revision => path.attributes['copyfrom-rev']\r
+ }\r
+ end\r
+ paths.sort! { |x,y| x[:path] <=> y[:path] }\r
+ \r
+ revisions << Revision.new({:identifier => logentry.attributes['revision'],\r
+ :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),\r
+ :time => Time.parse(logentry.elements['date'].text).localtime,\r
+ :message => logentry.elements['msg'].text,\r
+ :paths => paths\r
+ })\r
+ end\r
+ rescue\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ revisions\r
+ end\r
+ \r
+ def diff(path, identifier_from, identifier_to=nil, type="inline")\r
+ path ||= ''\r
+ identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''\r
+ identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)\r
+ \r
+ cmd = "#{SVN_BIN} diff -r "\r
+ cmd << "#{identifier_to}:"\r
+ cmd << "#{identifier_from}"\r
+ cmd << " #{target(URI.escape(path))}@#{identifier_from}"\r
+ cmd << credentials_string\r
+ diff = []\r
+ shellout(cmd) do |io|\r
+ io.each_line do |line|\r
+ diff << line\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ diff\r
+ end\r
+ \r
+ def cat(path, identifier=nil)\r
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+ cmd = "#{SVN_BIN} cat #{target(URI.escape(path))}@#{identifier}"\r
+ cmd << credentials_string\r
+ cat = nil\r
+ shellout(cmd) do |io|\r
+ io.binmode\r
+ cat = io.read\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ cat\r
+ end\r
+ \r
+ def annotate(path, identifier=nil)\r
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+ cmd = "#{SVN_BIN} blame #{target(URI.escape(path))}@#{identifier}"\r
+ cmd << credentials_string\r
+ blame = Annotate.new\r
+ shellout(cmd) do |io|\r
+ io.each_line do |line|\r
+ next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}\r
+ blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))\r
+ end\r
+ end\r
+ return nil if $? && $?.exitstatus != 0\r
+ blame\r
+ end\r
+ \r
+ private\r
+ \r
+ def credentials_string\r
+ str = ''\r
+ str << " --username #{shell_quote(@login)}" unless @login.blank?\r
+ str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?\r
+ str << " --no-auth-cache --non-interactive"\r
+ str\r
+ end\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+# Redmine - project management software\r
+# Copyright (C) 2006-2009 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+module Redmine\r
+ module Search\r
+ module Controller\r
+ def self.included(base)\r
+ base.extend(ClassMethods)\r
+ end\r
+\r
+ module ClassMethods\r
+ @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}\r
+ mattr_accessor :default_search_scopes\r
+ \r
+ # Set the default search scope for a controller or specific actions\r
+ # Examples:\r
+ # * search_scope :issues # => sets the search scope to :issues for the whole controller\r
+ # * search_scope :issues, :only => :index\r
+ # * search_scope :issues, :only => [:index, :show]\r
+ def default_search_scope(id, options = {})\r
+ if actions = options[:only]\r
+ actions = [] << actions unless actions.is_a?(Array)\r
+ actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}\r
+ else\r
+ default_search_scopes[controller_name.to_sym][:default] = id.to_s\r
+ end\r
+ end\r
+ end\r
+\r
+ def default_search_scopes\r
+ self.class.default_search_scopes\r
+ end\r
+\r
+ # Returns the default search scope according to the current action\r
+ def default_search_scope\r
+ @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||\r
+ default_search_scopes[controller_name.to_sym][:default]\r
+ end\r
+ end\r
+ end\r
+end\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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)
+ themes.find {|t| t.id == id}
+ end
+
+ # Class used to represent a theme
+ class Theme
+ attr_reader :name, :dir, :stylesheets
+
+ def initialize(path)
+ @dir = File.basename(path)
+ @name = @dir.humanize
+ @stylesheets = Dir.glob("#{path}/stylesheets/*.css").collect {|f| File.basename(f).gsub(/\.css$/, '')}
+ end
+
+ # Directory name used as the theme id
+ def id; dir end
+
+ def <=>(theme)
+ name <=> theme.name
+ end
+ end
+
+ private
+
+ def self.scan_themes
+ dirs = Dir.glob("#{RAILS_ROOT}/public/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 stylesheet_path(source)
+ @current_theme ||= Redmine::Themes.theme(Setting.ui_theme)
+ super((@current_theme && @current_theme.stylesheets.include?(source)) ?
+ "/themes/#{@current_theme.dir}/stylesheets/#{source}" : source)
+ end
+
+ def path_to_stylesheet(source)
+ stylesheet_path source
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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
+ # Class used to parse unified diffs
+ class UnifiedDiff < Array
+ def initialize(diff, options={})
+ options.assert_valid_keys(:type, :max_lines)
+ diff = diff.split("\n") if diff.is_a?(String)
+ diff_type = options[:type] || 'inline'
+
+ lines = 0
+ @truncated = false
+ diff_table = DiffTable.new(diff_type)
+ diff.each do |line|
+ unless diff_table.add_line line
+ self << diff_table if diff_table.length > 1
+ diff_table = DiffTable.new(diff_type)
+ end
+ lines += 1
+ if options[:max_lines] && lines > options[:max_lines]
+ @truncated = true
+ break
+ end
+ end
+ self << diff_table unless diff_table.empty?
+ self
+ end
+
+ def truncated?; @truncated; end
+ end
+
+ # Class that represents a file diff
+ class DiffTable < Hash
+ attr_reader :file_name, :line_num_l, :line_num_r
+
+ # Initialize with a Diff file and the type of Diff View
+ # The type view must be inline or sbs (side_by_side)
+ def initialize(type="inline")
+ @parsing = false
+ @nb_line = 1
+ @start = false
+ @before = 'same'
+ @second = true
+ @type = type
+ end
+
+ # Function for add a line of this Diff
+ # Returns false when the diff ends
+ def add_line(line)
+ unless @parsing
+ if line =~ /^(---|\+\+\+) (.*)$/
+ @file_name = $2
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $2.to_i
+ @line_num_r = $5.to_i
+ @parsing = true
+ end
+ else
+ if line =~ /^[^\+\-\s@\\]/
+ @parsing = false
+ return false
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $2.to_i
+ @line_num_r = $5.to_i
+ else
+ @nb_line += 1 if parse_line(line, @type)
+ end
+ end
+ return true
+ end
+
+ def inspect
+ puts '### DIFF TABLE ###'
+ puts "file : #{file_name}"
+ self.each do |d|
+ d.inspect
+ end
+ end
+
+ private
+ # Test if is a Side By Side type
+ def sbs?(type, func)
+ if @start and type == "sbs"
+ if @before == func and @second
+ tmp_nb_line = @nb_line
+ self[tmp_nb_line] = Diff.new
+ else
+ @second = false
+ tmp_nb_line = @start
+ @start += 1
+ @nb_line -= 1
+ end
+ else
+ tmp_nb_line = @nb_line
+ @start = @nb_line
+ self[tmp_nb_line] = Diff.new
+ @second = true
+ end
+ unless self[tmp_nb_line]
+ @nb_line += 1
+ self[tmp_nb_line] = Diff.new
+ else
+ self[tmp_nb_line]
+ end
+ end
+
+ # Escape the HTML for the diff
+ def escapeHTML(line)
+ CGI.escapeHTML(line)
+ end
+
+ def parse_line(line, type="inline")
+ if line[0, 1] == "+"
+ diff = sbs? type, 'add'
+ @before = 'add'
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.type_diff_right = 'diff_in'
+ @line_num_r += 1
+ true
+ elsif line[0, 1] == "-"
+ diff = sbs? type, 'remove'
+ @before = 'remove'
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ diff.type_diff_left = 'diff_out'
+ @line_num_l += 1
+ true
+ elsif line[0, 1] =~ /\s/
+ @before = 'same'
+ @start = false
+ diff = Diff.new
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ self[@nb_line] = diff
+ @line_num_l += 1
+ @line_num_r += 1
+ true
+ elsif line[0, 1] = "\\"
+ true
+ else
+ false
+ end
+ end
+ end
+
+ # A line of diff
+ class Diff
+ attr_accessor :nb_line_left
+ attr_accessor :line_left
+ attr_accessor :nb_line_right
+ attr_accessor :line_right
+ attr_accessor :type_diff_right
+ attr_accessor :type_diff_left
+
+ def initialize()
+ self.nb_line_left = ''
+ self.nb_line_right = ''
+ self.line_left = ''
+ self.line_right = ''
+ self.type_diff_right = ''
+ self.type_diff_left = ''
+ end
+
+ def inspect
+ puts '### Start Line Diff ###'
+ puts self.nb_line_left
+ puts self.line_left
+ puts self.nb_line_right
+ puts self.line_right
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Utils
+ class << self
+ # Returns the relative root url of the application
+ def relative_url_root
+ ActionController::Base.respond_to?('relative_url_root') ?
+ ActionController::Base.relative_url_root.to_s :
+ ActionController::AbstractRequest.relative_url_root.to_s
+ end
+
+ # Sets the relative root url of the application
+ def relative_url_root=(arg)
+ if ActionController::Base.respond_to?('relative_url_root=')
+ ActionController::Base.relative_url_root=arg
+ else
+ ActionController::AbstractRequest.relative_url_root=arg
+ end
+ end
+ end
+ end
+end
--- /dev/null
+require 'rexml/document'
+
+module Redmine
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 8
+ TINY = 7
+
+ # Branch values:
+ # * official release: nil
+ # * stable branch: stable
+ # * trunk: devel
+ BRANCH = 'devel'
+
+ def self.revision
+ revision = nil
+ entries_path = "#{RAILS_ROOT}/.svn/entries"
+ if File.readable?(entries_path)
+ begin
+ f = File.open(entries_path, 'r')
+ entries = f.read
+ f.close
+ if entries.match(%r{^\d+})
+ revision = $1.to_i if entries.match(%r{^\d+\s+dir\s+(\d+)\s})
+ else
+ xml = REXML::Document.new(entries)
+ revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i
+ end
+ rescue
+ # Could not find the current revision
+ end
+ end
+ revision
+ 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
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Views
+ module MyPage
+ module Block
+ def self.additional_blocks
+ @@additional_blocks ||= Dir.glob("#{RAILS_ROOT}/vendor/plugins/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
+ name = File.basename(file).split('.').first.gsub(/^_/, '')
+ h[name] = name.to_sym
+ h
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Views
+ class OtherFormatsBuilder
+ def initialize(view)
+ @view = view
+ end
+
+ def link_to(name, options={})
+ url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {})
+ caption = options.delete(:caption) || name
+ html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options)
+ @view.content_tag('span', @view.link_to(caption, url, html_options))
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module WikiFormatting
+ @@formatters = {}
+
+ class << self
+ def map
+ yield self
+ end
+
+ def register(name, formatter, helper)
+ raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name.to_sym]
+ @@formatters[name.to_sym] = {:formatter => formatter, :helper => helper}
+ end
+
+ def formatter_for(name)
+ entry = @@formatters[name.to_sym]
+ (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
+ end
+
+ def helper_for(name)
+ entry = @@formatters[name.to_sym]
+ (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
+ end
+
+ def format_names
+ @@formatters.keys.map
+ end
+
+ def to_html(format, text, options = {}, &block)
+ formatter_for(format).new(text).to_html(&block)
+ end
+ end
+
+ # Default formatter module
+ module NullFormatter
+ class Formatter
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::UrlHelper
+
+ def initialize(text)
+ @text = text
+ end
+
+ def to_html(*args)
+ simple_format(auto_link(CGI::escapeHTML(@text)))
+ end
+ end
+
+ module Helper
+ def wikitoolbar_for(field_id)
+ end
+
+ def heads_for_wiki_formatter
+ end
+
+ def initial_page_content(page)
+ page.pretty_title.to_s
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module WikiFormatting
+ module Macros
+ module Definitions
+ def exec_macro(name, obj, args)
+ method_name = "macro_#{name}"
+ send(method_name, obj, args) if respond_to?(method_name)
+ end
+
+ def extract_macro_options(args, *keys)
+ options = {}
+ while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
+ options[$1.downcase.to_sym] = $2
+ args.pop
+ end
+ return [args, options]
+ end
+ end
+
+ @@available_macros = {}
+
+ class << self
+ # Called with a block to define additional macros.
+ # Macro blocks accept 2 arguments:
+ # * obj: the object that is rendered
+ # * args: macro arguments
+ #
+ # Plugins can use this method to define new macros:
+ #
+ # Redmine::WikiFormatting::Macros.register do
+ # desc "This is my macro"
+ # macro :my_macro do |obj, args|
+ # "My macro output"
+ # end
+ # end
+ def register(&block)
+ class_eval(&block) if block_given?
+ end
+
+ private
+ # Defines a new macro with the given name and block.
+ def macro(name, &block)
+ name = name.to_sym if name.is_a?(String)
+ @@available_macros[name] = @@desc || ''
+ @@desc = nil
+ raise "Can not create a macro without a block!" unless block_given?
+ Definitions.send :define_method, "macro_#{name}".downcase, &block
+ end
+
+ # Sets description for the next macro to be defined
+ def desc(txt)
+ @@desc = txt
+ end
+ end
+
+ # Builtin macros
+ desc "Sample macro."
+ macro :hello_world do |obj, args|
+ "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
+ end
+
+ desc "Displays a list of all available macros, including description if available."
+ macro :macro_list do
+ out = ''
+ @@available_macros.keys.collect(&:to_s).sort.each do |macro|
+ out << content_tag('dt', content_tag('code', macro))
+ out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
+ end
+ content_tag('dl', out)
+ end
+
+ desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
+ " !{{child_pages}} -- can be used from a wiki page only\n" +
+ " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
+ " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
+ macro :child_pages do |obj, args|
+ args, options = extract_macro_options(args, :parent)
+ page = nil
+ if args.size > 0
+ page = Wiki.find_page(args.first.to_s, :project => @project)
+ elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
+ page = obj.page
+ else
+ raise 'With no argument, this macro can be called from wiki pages only.'
+ end
+ raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
+ pages = ([page] + page.descendants).group_by(&:parent_id)
+ render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
+ end
+
+ desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
+ macro :include do |obj, args|
+ page = Wiki.find_page(args.first.to_s, :project => @project)
+ raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
+ @included_wiki_pages ||= []
+ raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
+ @included_wiki_pages << page.title
+ out = textilizable(page.content, :text, :attachments => page.attachments)
+ @included_wiki_pages.pop
+ out
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redcloth3'
+require 'coderay'
+
+module Redmine
+ module WikiFormatting
+ module Textile
+ class Formatter < RedCloth3
+
+ # auto_link rule after textile rules so that it doesn't break !image_url! tags
+ RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
+
+ def initialize(*args)
+ super
+ self.hard_breaks=true
+ self.no_span_caps=true
+ self.filter_styles=true
+ end
+
+ def to_html(*rules, &block)
+ @toc = []
+ @macros_runner = block
+ super(*RULES).to_s
+ end
+
+ private
+
+ # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
+ # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
+ def hard_break( text )
+ text.gsub!( /(.)\n(?!\n|\Z|>| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
+ end
+
+ # Patch to add code highlighting support to RedCloth
+ def smooth_offtags( text )
+ unless @pre_list.empty?
+ ## replace <pre> content
+ text.gsub!(/<redpre#(\d+)>/) do
+ content = @pre_list[$1.to_i]
+ if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
+ content = "<code class=\"#{$1} CodeRay\">" +
+ CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
+ end
+ content
+ end
+ end
+ end
+
+ # Patch to add 'table of content' support to RedCloth
+ def textile_p_withtoc(tag, atts, cite, content)
+ # removes wiki links from the item
+ toc_item = content.gsub(/(\[\[([^\]\|]*)(\|([^\]]*))?\]\])/) { $4 || $2 }
+ # removes styles
+ # eg. %{color:red}Triggers% => Triggers
+ toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
+
+ # replaces non word caracters by dashes
+ anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
+
+ unless anchor.blank?
+ if tag =~ /^h(\d)$/
+ @toc << [$1.to_i, anchor, toc_item]
+ end
+ atts << " id=\"#{anchor}\""
+ content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a>"
+ end
+ textile_p(tag, atts, cite, content)
+ end
+
+ alias :textile_h1 :textile_p_withtoc
+ alias :textile_h2 :textile_p_withtoc
+ alias :textile_h3 :textile_p_withtoc
+
+ def inline_toc(text)
+ text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
+ div_class = 'toc'
+ div_class << ' right' if $1 == '>'
+ div_class << ' left' if $1 == '<'
+ out = "<ul class=\"#{div_class}\">"
+ @toc.each do |heading|
+ level, anchor, toc_item = heading
+ out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
+ end
+ out << '</ul>'
+ out
+ end
+ end
+
+ MACROS_RE = /
+ (!)? # escaping
+ (
+ \{\{ # opening tag
+ ([\w]+) # macro name
+ (\(([^\}]*)\))? # optional arguments
+ \}\} # closing tag
+ )
+ /x unless const_defined?(:MACROS_RE)
+
+ def inline_macros(text)
+ text.gsub!(MACROS_RE) do
+ esc, all, macro = $1, $2, $3.downcase
+ args = ($5 || '').split(',').each(&:strip)
+ if esc.nil?
+ begin
+ @macros_runner.call(macro, args)
+ rescue => e
+ "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
+ end || all
+ else
+ all
+ end
+ end
+ end
+
+ AUTO_LINK_RE = %r{
+ ( # leading text
+ <\w+.*?>| # leading HTML tag, or
+ [^=<>!:'"/]| # leading punctuation, or
+ ^ # beginning of line
+ )
+ (
+ (?:https?://)| # protocol spec, or
+ (?:s?ftps?://)|
+ (?:www\.) # www.*
+ )
+ (
+ (\S+?) # url
+ (\/)? # slash
+ )
+ ([^\w\=\/;\(\)]*?) # post
+ (?=<|\s|$)
+ }x unless const_defined?(:AUTO_LINK_RE)
+
+ # Turns all urls into clickable links (code from Rails).
+ def inline_auto_link(text)
+ text.gsub!(AUTO_LINK_RE) do
+ all, leading, proto, url, post = $&, $1, $2, $3, $6
+ if leading =~ /<a\s/i || leading =~ /![<>=]?/
+ # don't replace URL's that are already linked
+ # and URL's prefixed with ! !> !< != (textile images)
+ all
+ else
+ # Idea below : an URL with unbalanced parethesis and
+ # ending by ')' is put into external parenthesis
+ if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
+ url=url[0..-2] # discard closing parenth from url
+ post = ")"+post # add closing parenth to post
+ end
+ %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
+ end
+ end
+ end
+
+ # Turns all email addresses into clickable links (code from Rails).
+ def inline_auto_mailto(text)
+ text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
+ mail = $1
+ if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
+ mail
+ else
+ %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
+ end
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module WikiFormatting
+ module Textile
+ module Helper
+ def wikitoolbar_for(field_id)
+ # Is there a simple way to link to a public resource?
+ url = "#{Redmine::Utils.relative_url_root}/help/wiki_syntax.html"
+
+ help_link = l(:setting_text_formatting) + ': ' +
+ link_to(l(:label_help), url,
+ :onclick => "window.open(\"#{ url }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
+
+ javascript_include_tag('jstoolbar/jstoolbar') +
+ javascript_include_tag('jstoolbar/textile') +
+ javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
+ javascript_tag("var wikiToolbar = new jsToolBar($('#{field_id}')); wikiToolbar.setHelpLink('#{help_link}'); wikiToolbar.draw();")
+ end
+
+ def initial_page_content(page)
+ "h1. #{@page.pretty_title}"
+ end
+
+ def heads_for_wiki_formatter
+ stylesheet_link_tag 'jstoolbar'
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'action_view/helpers/form_helper'
+
+class TabularFormBuilder < ActionView::Helpers::FormBuilder
+ include Redmine::I18n
+
+ def initialize(object_name, object, template, options, proc)
+ set_language_if_valid options.delete(:lang)
+ super
+ end
+
+ (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
+ src = <<-END_SRC
+ def #{selector}(field, options = {})
+ label_for_field(field, options) + super
+ end
+ END_SRC
+ class_eval src, __FILE__, __LINE__
+ end
+
+ def select(field, choices, options = {}, html_options = {})
+ label_for_field(field, options) + super
+ end
+
+ # Returns a label tag for the given field
+ def label_for_field(field, options = {})
+ return '' if options.delete(:no_label)
+ text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
+ text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
+ text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
+ @template.content_tag("label", text,
+ :class => (@object && @object.errors[field] ? "error" : nil),
+ :for => (@object_name.to_s + "_" + field.to_s))
+ end
+end
--- /dev/null
+def deprecated_task(name, new_name)
+ task name=>new_name do
+ $stderr.puts "\nNote: The rake task #{name} has been deprecated, please use the replacement version #{new_name}"
+ end
+end
+
+deprecated_task :load_default_data, "redmine:load_default_data"
+deprecated_task :migrate_from_mantis, "redmine:migrate_from_mantis"
+deprecated_task :migrate_from_trac, "redmine:migrate_from_trac"
--- /dev/null
+# Redmine - project management software\r
+# Copyright (C) 2006-2008 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+namespace :redmine do\r
+ namespace :email do\r
+\r
+ desc <<-END_DESC\r
+Read an email from standard input.\r
+\r
+General options:\r
+ unknown_user=ACTION how to handle emails from an unknown user\r
+ ACTION can be one of the following values:\r
+ ignore: email is ignored (default)\r
+ accept: accept as anonymous user\r
+ create: create a user account\r
+ \r
+Issue attributes control options:\r
+ project=PROJECT identifier of the target project\r
+ status=STATUS name of the target status\r
+ tracker=TRACKER name of the target tracker\r
+ category=CATEGORY name of the target category\r
+ priority=PRIORITY name of the target priority\r
+ allow_override=ATTRS allow email content to override attributes\r
+ specified by previous options\r
+ ATTRS is a comma separated list of attributes\r
+\r
+Examples:\r
+ # No project specified. Emails MUST contain the 'Project' keyword:\r
+ rake redmine:email:read RAILS_ENV="production" < raw_email\r
+\r
+ # Fixed project and default tracker specified, but emails can override\r
+ # both tracker and priority attributes:\r
+ rake redmine:email:read RAILS_ENV="production" \\\r
+ project=foo \\\r
+ tracker=bug \\\r
+ allow_override=tracker,priority < raw_email\r
+END_DESC\r
+\r
+ task :read => :environment do\r
+ options = { :issue => {} }\r
+ %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }\r
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']\r
+ options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']\r
+ \r
+ MailHandler.receive(STDIN.read, options)\r
+ end\r
+ \r
+ desc <<-END_DESC\r
+Read emails from an IMAP server.\r
+\r
+General options:\r
+ unknown_user=ACTION how to handle emails from an unknown user\r
+ ACTION can be one of the following values:\r
+ ignore: email is ignored (default)\r
+ accept: accept as anonymous user\r
+ create: create a user account\r
+ \r
+Available IMAP options:\r
+ host=HOST IMAP server host (default: 127.0.0.1)\r
+ port=PORT IMAP server port (default: 143)\r
+ ssl=SSL Use SSL? (default: false)\r
+ username=USERNAME IMAP account\r
+ password=PASSWORD IMAP password\r
+ folder=FOLDER IMAP folder to read (default: INBOX)\r
+ \r
+Issue attributes control options:\r
+ project=PROJECT identifier of the target project\r
+ status=STATUS name of the target status\r
+ tracker=TRACKER name of the target tracker\r
+ category=CATEGORY name of the target category\r
+ priority=PRIORITY name of the target priority\r
+ allow_override=ATTRS allow email content to override attributes\r
+ specified by previous options\r
+ ATTRS is a comma separated list of attributes\r
+ \r
+Processed emails control options:\r
+ move_on_success=MAILBOX move emails that were successfully received\r
+ to MAILBOX instead of deleting them\r
+ move_on_failure=MAILBOX move emails that were ignored to MAILBOX\r
+ \r
+Examples:\r
+ # No project specified. Emails MUST contain the 'Project' keyword:\r
+ \r
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\\r
+ host=imap.foo.bar username=redmine@example.net password=xxx\r
+\r
+\r
+ # Fixed project and default tracker specified, but emails can override\r
+ # both tracker and priority attributes:\r
+ \r
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\\r
+ host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\\r
+ project=foo \\\r
+ tracker=bug \\\r
+ allow_override=tracker,priority\r
+END_DESC\r
+\r
+ task :receive_imap => :environment do\r
+ imap_options = {:host => ENV['host'],\r
+ :port => ENV['port'],\r
+ :ssl => ENV['ssl'],\r
+ :username => ENV['username'],\r
+ :password => ENV['password'],\r
+ :folder => ENV['folder'],\r
+ :move_on_success => ENV['move_on_success'],\r
+ :move_on_failure => ENV['move_on_failure']}\r
+ \r
+ options = { :issue => {} }\r
+ %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }\r
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']\r
+ options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']\r
+\r
+ Redmine::IMAP.check(imap_options, options)\r
+ end\r
+ end\r
+end\r
--- /dev/null
+desc 'Create YAML test fixtures from data in an existing database.\r
+Defaults to development database. Set RAILS_ENV to override.'\r
+\r
+task :extract_fixtures => :environment do\r
+ sql = "SELECT * FROM %s"\r
+ skip_tables = ["schema_info"]\r
+ ActiveRecord::Base.establish_connection\r
+ (ActiveRecord::Base.connection.tables - skip_tables).each do |table_name|\r
+ i = "000"\r
+ File.open("#{RAILS_ROOT}/#{table_name}.yml", 'w' ) do |file|\r
+ data = ActiveRecord::Base.connection.select_all(sql % table_name)\r
+ file.write data.inject({}) { |hash, record|\r
+ \r
+ # cast extracted values\r
+ ActiveRecord::Base.connection.columns(table_name).each { |col|\r
+ record[col.name] = col.type_cast(record[col.name]) if record[col.name] \r
+ } \r
+ \r
+ hash["#{table_name}_#{i.succ!}"] = record\r
+ hash\r
+ }.to_yaml\r
+ end\r
+ end\r
+end
\ No newline at end of file
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006-2008 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+desc 'Fetch changesets from the repositories'\r
+\r
+namespace :redmine do\r
+ task :fetch_changesets => :environment do\r
+ Repository.fetch_changesets\r
+ end\r
+end\r
--- /dev/null
+desc 'Generates a configuration file for cookie store sessions.'
+
+file 'config/initializers/session_store.rb' do
+ path = File.join(RAILS_ROOT, 'config', 'initializers', 'session_store.rb')
+ secret = ActiveSupport::SecureRandom.hex(40)
+ File.open(path, 'w') do |f|
+ f.write <<"EOF"
+# This file was generated by 'rake config/initializers/session_store.rb',
+# and should not be made visible to public.
+# If you have a load-balancing Redmine cluster, you will need to use the
+# same version of this file on each machine. And be sure to restart your
+# server when you modify this file.
+
+# Your secret key for verifying cookie session data integrity. If you
+# change this key, all old sessions will become invalid! Make sure the
+# secret is at least 30 characters and all random, no regular words or
+# you'll be exposed to dictionary attacks.
+ActionController::Base.session = {
+ :session_key => '_redmine_session',
+ :secret => '#{secret}'
+}
+EOF
+ end
+end
--- /dev/null
+desc 'Load Redmine default configuration data. Language is chosen interactively or by setting REDMINE_LANG environment variable.'\r
+\r
+namespace :redmine do\r
+ task :load_default_data => :environment do\r
+ include Redmine::I18n\r
+ set_language_if_valid('en')\r
+ \r
+ envlang = ENV['REDMINE_LANG']\r
+ if !envlang || !set_language_if_valid(envlang)\r
+ puts\r
+ while true\r
+ print "Select language: "\r
+ print valid_languages.collect(&:to_s).sort.join(", ")\r
+ print " [#{current_language}] "\r
+ STDOUT.flush\r
+ lang = STDIN.gets.chomp!\r
+ break if lang.empty?\r
+ break if set_language_if_valid(lang)\r
+ puts "Unknown language!"\r
+ end\r
+ STDOUT.flush\r
+ puts "===================================="\r
+ end\r
+ \r
+ begin\r
+ Redmine::DefaultData::Loader.load(current_language)\r
+ puts "Default configuration data loaded."\r
+ rescue Redmine::DefaultData::DataAlreadyLoaded => error\r
+ puts error\r
+ rescue => error\r
+ puts "Error: " + error\r
+ puts "Default configuration data was not loaded."\r
+ end\r
+ end\r
+end\r
--- /dev/null
+namespace :locales do
+ desc 'Updates language files based on en.yml content (only works for new top level keys).'
+ task :update do
+ dir = ENV['DIR'] || './config/locales'
+
+ en_strings = YAML.load(File.read(File.join(dir,'en.yml')))['en']
+
+ files = Dir.glob(File.join(dir,'*.{yaml,yml}'))
+ files.each do |file|
+ puts "Updating file #{file}"
+ file_strings = YAML.load(File.read(file))
+ file_strings = file_strings[file_strings.keys.first]
+
+ missing_keys = en_strings.keys - file_strings.keys
+ next if missing_keys.empty?
+
+ puts "==> Missing #{missing_keys.size} keys (#{missing_keys.join(', ')})"
+ lang = File.open(file, 'a')
+
+ missing_keys.each do |key|
+ {key => en_strings[key]}.to_yaml.each_line do |line|
+ next if line =~ /^---/ || line.empty?
+ puts " #{line}"
+ lang << " #{line}"
+ end
+ end
+
+ lang.close
+ end
+ end
+end
--- /dev/null
+begin
+ require 'metric_fu'
+rescue LoadError
+ # Metric-fu not installed
+ # http://metric-fu.rubyforge.org/
+end
--- /dev/null
+# redMine - project management software\r
+# Copyright (C) 2006-2007 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+desc 'Mantis migration script'\r
+\r
+require 'active_record'\r
+require 'iconv'\r
+require 'pp'\r
+\r
+namespace :redmine do\r
+task :migrate_from_mantis => :environment do\r
+ \r
+ module MantisMigrate\r
+ \r
+ DEFAULT_STATUS = IssueStatus.default\r
+ assigned_status = IssueStatus.find_by_position(2)\r
+ resolved_status = IssueStatus.find_by_position(3)\r
+ feedback_status = IssueStatus.find_by_position(4)\r
+ closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }\r
+ STATUS_MAPPING = {10 => DEFAULT_STATUS, # new\r
+ 20 => feedback_status, # feedback\r
+ 30 => DEFAULT_STATUS, # acknowledged\r
+ 40 => DEFAULT_STATUS, # confirmed\r
+ 50 => assigned_status, # assigned\r
+ 80 => resolved_status, # resolved\r
+ 90 => closed_status # closed\r
+ }\r
+ \r
+ priorities = Enumeration.priorities\r
+ DEFAULT_PRIORITY = priorities[2]\r
+ PRIORITY_MAPPING = {10 => priorities[1], # none\r
+ 20 => priorities[1], # low\r
+ 30 => priorities[2], # normal\r
+ 40 => priorities[3], # high\r
+ 50 => priorities[4], # urgent\r
+ 60 => priorities[5] # immediate\r
+ }\r
+ \r
+ TRACKER_BUG = Tracker.find_by_position(1)\r
+ TRACKER_FEATURE = Tracker.find_by_position(2)\r
+ \r
+ roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')\r
+ manager_role = roles[0]\r
+ developer_role = roles[1]\r
+ DEFAULT_ROLE = roles.last\r
+ ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer\r
+ 25 => DEFAULT_ROLE, # reporter\r
+ 40 => DEFAULT_ROLE, # updater\r
+ 55 => developer_role, # developer\r
+ 70 => manager_role, # manager\r
+ 90 => manager_role # administrator\r
+ }\r
+ \r
+ CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String\r
+ 1 => 'int', # Numeric\r
+ 2 => 'int', # Float\r
+ 3 => 'list', # Enumeration\r
+ 4 => 'string', # Email\r
+ 5 => 'bool', # Checkbox\r
+ 6 => 'list', # List\r
+ 7 => 'list', # Multiselection list\r
+ 8 => 'date', # Date\r
+ }\r
+ \r
+ RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to\r
+ 2 => IssueRelation::TYPE_RELATES, # parent of\r
+ 3 => IssueRelation::TYPE_RELATES, # child of\r
+ 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of\r
+ 4 => IssueRelation::TYPE_DUPLICATES # has duplicate\r
+ }\r
+ \r
+ class MantisUser < ActiveRecord::Base\r
+ set_table_name :mantis_user_table\r
+ \r
+ def firstname\r
+ @firstname = realname.blank? ? username : realname.split.first[0..29]\r
+ @firstname.gsub!(/[^\w\s\'\-]/i, '')\r
+ @firstname\r
+ end\r
+ \r
+ def lastname\r
+ @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]\r
+ @lastname.gsub!(/[^\w\s\'\-]/i, '')\r
+ @lastname = '-' if @lastname.blank?\r
+ @lastname\r
+ end\r
+ \r
+ def email\r
+ if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&\r
+ !User.find_by_mail(read_attribute(:email))\r
+ @email = read_attribute(:email)\r
+ else\r
+ @email = "#{username}@foo.bar"\r
+ end\r
+ end\r
+ \r
+ def username\r
+ read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')\r
+ end\r
+ end\r
+ \r
+ class MantisProject < ActiveRecord::Base\r
+ set_table_name :mantis_project_table\r
+ has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id\r
+ has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id\r
+ has_many :news, :class_name => "MantisNews", :foreign_key => :project_id\r
+ has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id\r
+ \r
+ def name\r
+ read_attribute(:name)[0..29]\r
+ end\r
+ \r
+ def identifier\r
+ read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')\r
+ end\r
+ end\r
+ \r
+ class MantisVersion < ActiveRecord::Base\r
+ set_table_name :mantis_project_version_table\r
+ \r
+ def version\r
+ read_attribute(:version)[0..29]\r
+ end\r
+ \r
+ def description\r
+ read_attribute(:description)[0..254]\r
+ end\r
+ end\r
+ \r
+ class MantisCategory < ActiveRecord::Base\r
+ set_table_name :mantis_project_category_table\r
+ end\r
+ \r
+ class MantisProjectUser < ActiveRecord::Base\r
+ set_table_name :mantis_project_user_list_table\r
+ end\r
+ \r
+ class MantisBug < ActiveRecord::Base\r
+ set_table_name :mantis_bug_table\r
+ belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id\r
+ has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id\r
+ has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id\r
+ has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id\r
+ end\r
+ \r
+ class MantisBugText < ActiveRecord::Base\r
+ set_table_name :mantis_bug_text_table\r
+ \r
+ # Adds Mantis steps_to_reproduce and additional_information fields\r
+ # to description if any\r
+ def full_description\r
+ full_description = description\r
+ full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?\r
+ full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?\r
+ full_description\r
+ end\r
+ end\r
+ \r
+ class MantisBugNote < ActiveRecord::Base\r
+ set_table_name :mantis_bugnote_table\r
+ belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id\r
+ belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id\r
+ end\r
+ \r
+ class MantisBugNoteText < ActiveRecord::Base\r
+ set_table_name :mantis_bugnote_text_table\r
+ end\r
+ \r
+ class MantisBugFile < ActiveRecord::Base\r
+ set_table_name :mantis_bug_file_table\r
+ \r
+ def size\r
+ filesize\r
+ end\r
+ \r
+ def original_filename\r
+ MantisMigrate.encode(filename)\r
+ end\r
+ \r
+ def content_type\r
+ file_type\r
+ end\r
+ \r
+ def read(*args)\r
+ if @read_finished\r
+ nil\r
+ else\r
+ @read_finished = true\r
+ content\r
+ end\r
+ end\r
+ end\r
+ \r
+ class MantisBugRelationship < ActiveRecord::Base\r
+ set_table_name :mantis_bug_relationship_table\r
+ end\r
+ \r
+ class MantisBugMonitor < ActiveRecord::Base\r
+ set_table_name :mantis_bug_monitor_table\r
+ end\r
+ \r
+ class MantisNews < ActiveRecord::Base\r
+ set_table_name :mantis_news_table\r
+ end\r
+ \r
+ class MantisCustomField < ActiveRecord::Base\r
+ set_table_name :mantis_custom_field_table\r
+ set_inheritance_column :none \r
+ has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id\r
+ has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id\r
+ \r
+ def format\r
+ read_attribute :type\r
+ end\r
+ \r
+ def name\r
+ read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')\r
+ end\r
+ end\r
+ \r
+ class MantisCustomFieldProject < ActiveRecord::Base\r
+ set_table_name :mantis_custom_field_project_table \r
+ end\r
+ \r
+ class MantisCustomFieldString < ActiveRecord::Base\r
+ set_table_name :mantis_custom_field_string_table \r
+ end\r
+ \r
+ \r
+ def self.migrate\r
+ \r
+ # Users\r
+ print "Migrating users"\r
+ User.delete_all "login <> 'admin'"\r
+ users_map = {}\r
+ users_migrated = 0\r
+ MantisUser.find(:all).each do |user|\r
+ u = User.new :firstname => encode(user.firstname), \r
+ :lastname => encode(user.lastname),\r
+ :mail => user.email,\r
+ :last_login_on => user.last_visit\r
+ u.login = user.username\r
+ u.password = 'mantis'\r
+ u.status = User::STATUS_LOCKED if user.enabled != 1\r
+ u.admin = true if user.access_level == 90\r
+ next unless u.save!\r
+ users_migrated += 1\r
+ users_map[user.id] = u.id\r
+ print '.'\r
+ end\r
+ puts\r
+ \r
+ # Projects\r
+ print "Migrating projects"\r
+ Project.destroy_all\r
+ projects_map = {}\r
+ versions_map = {}\r
+ categories_map = {}\r
+ MantisProject.find(:all).each do |project|\r
+ p = Project.new :name => encode(project.name), \r
+ :description => encode(project.description)\r
+ p.identifier = project.identifier\r
+ next unless p.save\r
+ projects_map[project.id] = p.id\r
+ p.enabled_module_names = ['issue_tracking', 'news', 'wiki']\r
+ p.trackers << TRACKER_BUG\r
+ p.trackers << TRACKER_FEATURE\r
+ print '.'\r
+ \r
+ # Project members\r
+ project.members.each do |member|\r
+ m = Member.new :user => User.find_by_id(users_map[member.user_id]),\r
+ :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]\r
+ m.project = p\r
+ m.save\r
+ end \r
+ \r
+ # Project versions\r
+ project.versions.each do |version|\r
+ v = Version.new :name => encode(version.version),\r
+ :description => encode(version.description),\r
+ :effective_date => version.date_order.to_date\r
+ v.project = p\r
+ v.save\r
+ versions_map[version.id] = v.id\r
+ end\r
+ \r
+ # Project categories\r
+ project.categories.each do |category|\r
+ g = IssueCategory.new :name => category.category[0,30]\r
+ g.project = p\r
+ g.save\r
+ categories_map[category.category] = g.id\r
+ end\r
+ end \r
+ puts \r
+ \r
+ # Bugs\r
+ print "Migrating bugs"\r
+ Issue.destroy_all\r
+ issues_map = {}\r
+ keep_bug_ids = (Issue.count == 0)\r
+ MantisBug.find_each(:batch_size => 200) do |bug|\r
+ next unless projects_map[bug.project_id] && users_map[bug.reporter_id]\r
+ i = Issue.new :project_id => projects_map[bug.project_id], \r
+ :subject => encode(bug.summary),\r
+ :description => encode(bug.bug_text.full_description),\r
+ :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,\r
+ :created_on => bug.date_submitted,\r
+ :updated_on => bug.last_updated\r
+ i.author = User.find_by_id(users_map[bug.reporter_id])\r
+ i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?\r
+ i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?\r
+ i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS\r
+ i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)\r
+ i.id = bug.id if keep_bug_ids\r
+ next unless i.save\r
+ issues_map[bug.id] = i.id\r
+ print '.'\r
+\r
+ # Assignee\r
+ # Redmine checks that the assignee is a project member\r
+ if (bug.handler_id && users_map[bug.handler_id])\r
+ i.assigned_to = User.find_by_id(users_map[bug.handler_id])\r
+ i.save_with_validation(false)\r
+ end \r
+ \r
+ # Bug notes\r
+ bug.bug_notes.each do |note|\r
+ next unless users_map[note.reporter_id]\r
+ n = Journal.new :notes => encode(note.bug_note_text.note),\r
+ :created_on => note.date_submitted\r
+ n.user = User.find_by_id(users_map[note.reporter_id])\r
+ n.journalized = i\r
+ n.save\r
+ end\r
+ \r
+ # Bug files\r
+ bug.bug_files.each do |file|\r
+ a = Attachment.new :created_on => file.date_added\r
+ a.file = file\r
+ a.author = User.find :first\r
+ a.container = i\r
+ a.save\r
+ end\r
+ \r
+ # Bug monitors\r
+ bug.bug_monitors.each do |monitor|\r
+ next unless users_map[monitor.user_id]\r
+ i.add_watcher(User.find_by_id(users_map[monitor.user_id]))\r
+ end\r
+ end\r
+ \r
+ # update issue id sequence if needed (postgresql)\r
+ Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')\r
+ puts\r
+ \r
+ # Bug relationships\r
+ print "Migrating bug relations"\r
+ MantisBugRelationship.find(:all).each do |relation|\r
+ next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]\r
+ r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]\r
+ r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])\r
+ r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])\r
+ pp r unless r.save\r
+ print '.'\r
+ end\r
+ puts\r
+ \r
+ # News\r
+ print "Migrating news"\r
+ News.destroy_all\r
+ MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|\r
+ next unless projects_map[news.project_id]\r
+ n = News.new :project_id => projects_map[news.project_id],\r
+ :title => encode(news.headline[0..59]),\r
+ :description => encode(news.body),\r
+ :created_on => news.date_posted\r
+ n.author = User.find_by_id(users_map[news.poster_id])\r
+ n.save\r
+ print '.'\r
+ end\r
+ puts\r
+ \r
+ # Custom fields\r
+ print "Migrating custom fields"\r
+ IssueCustomField.destroy_all\r
+ MantisCustomField.find(:all).each do |field|\r
+ f = IssueCustomField.new :name => field.name[0..29],\r
+ :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],\r
+ :min_length => field.length_min,\r
+ :max_length => field.length_max,\r
+ :regexp => field.valid_regexp,\r
+ :possible_values => field.possible_values.split('|'),\r
+ :is_required => field.require_report?\r
+ next unless f.save\r
+ print '.'\r
+ \r
+ # Trackers association\r
+ f.trackers = Tracker.find :all\r
+ \r
+ # Projects association\r
+ field.projects.each do |project|\r
+ f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]\r
+ end\r
+ \r
+ # Values\r
+ field.values.each do |value|\r
+ v = CustomValue.new :custom_field_id => f.id,\r
+ :value => value.value\r
+ v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]\r
+ v.save\r
+ end unless f.new_record?\r
+ end\r
+ puts\r
+ \r
+ puts\r
+ puts "Users: #{users_migrated}/#{MantisUser.count}"\r
+ puts "Projects: #{Project.count}/#{MantisProject.count}"\r
+ puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"\r
+ puts "Versions: #{Version.count}/#{MantisVersion.count}"\r
+ puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"\r
+ puts "Bugs: #{Issue.count}/#{MantisBug.count}"\r
+ puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"\r
+ puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"\r
+ puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"\r
+ puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"\r
+ puts "News: #{News.count}/#{MantisNews.count}"\r
+ puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"\r
+ end\r
+ \r
+ def self.encoding(charset)\r
+ @ic = Iconv.new('UTF-8', charset)\r
+ rescue Iconv::InvalidEncoding\r
+ return false \r
+ end\r
+ \r
+ def self.establish_connection(params)\r
+ constants.each do |const|\r
+ klass = const_get(const)\r
+ next unless klass.respond_to? 'establish_connection'\r
+ klass.establish_connection params\r
+ end\r
+ end\r
+ \r
+ def self.encode(text)\r
+ @ic.iconv text\r
+ rescue\r
+ text\r
+ end\r
+ end\r
+ \r
+ puts\r
+ if Redmine::DefaultData::Loader.no_data?\r
+ puts "Redmine configuration need to be loaded before importing data."\r
+ puts "Please, run this first:"\r
+ puts\r
+ puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""\r
+ exit\r
+ end\r
+ \r
+ puts "WARNING: Your Redmine data will be deleted during this process."\r
+ print "Are you sure you want to continue ? [y/N] "\r
+ break unless STDIN.gets.match(/^y$/i)\r
+ \r
+ # Default Mantis database settings\r
+ db_params = {:adapter => 'mysql', \r
+ :database => 'bugtracker', \r
+ :host => 'localhost', \r
+ :username => 'root', \r
+ :password => '' }\r
+\r
+ puts \r
+ puts "Please enter settings for your Mantis database" \r
+ [:adapter, :host, :database, :username, :password].each do |param|\r
+ print "#{param} [#{db_params[param]}]: "\r
+ value = STDIN.gets.chomp!\r
+ db_params[param] = value unless value.blank?\r
+ end\r
+ \r
+ while true\r
+ print "encoding [UTF-8]: "\r
+ encoding = STDIN.gets.chomp!\r
+ encoding = 'UTF-8' if encoding.blank?\r
+ break if MantisMigrate.encoding encoding\r
+ puts "Invalid encoding!"\r
+ end\r
+ puts\r
+ \r
+ # Make sure bugs can refer bugs in other projects\r
+ Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'\r
+ \r
+ # Turn off email notifications\r
+ Setting.notified_events = []\r
+ \r
+ MantisMigrate.establish_connection db_params\r
+ MantisMigrate.migrate\r
+end\r
+end\r
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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'
+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.find :first, :conditions => { :is_closed => true }
+ STATUS_MAPPING = {'new' => DEFAULT_STATUS,
+ 'reopened' => feedback_status,
+ 'assigned' => assigned_status,
+ 'closed' => closed_status
+ }
+
+ priorities = Enumeration.priorities
+ 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.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
+ 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
+ set_table_name :component
+ end
+
+ class TracMilestone < ActiveRecord::Base
+ set_table_name :milestone
+ # If this attribute is set a milestone has a defined target timepoint
+ 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
+ set_table_name :ticket_custom
+ end
+
+ class TracAttachment < ActiveRecord::Base
+ set_table_name :attachment
+ set_inheritance_column :none
+
+ def time; Time.at(read_attribute(:time)) end
+
+ def original_filename
+ filename
+ end
+
+ def content_type
+ Redmine::MimeType.of(filename) || ''
+ end
+
+ def exist?
+ File.file? trac_fullpath
+ end
+
+ def 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
+ set_table_name :ticket
+ set_inheritance_column :none
+
+ # ticket changes: only migrate status changes and comments
+ has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
+ has_many :attachments, :class_name => "TracAttachment",
+ :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
+ " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
+ ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
+ has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
+
+ def ticket_type
+ read_attribute(:type)
+ end
+
+ def summary
+ read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
+ end
+
+ def description
+ read_attribute(:description).blank? ? summary : read_attribute(:description)
+ end
+
+ def time; Time.at(read_attribute(:time)) end
+ def changetime; Time.at(read_attribute(:changetime)) end
+ end
+
+ class TracTicketChange < ActiveRecord::Base
+ set_table_name :ticket_change
+
+ def time; Time.at(read_attribute(:time)) end
+ end
+
+ TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
+ TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
+ TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
+ 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
+ set_table_name :wiki
+ set_primary_key :name
+
+ has_many :attachments, :class_name => "TracAttachment",
+ :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
+ " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
+ ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
+
+ def self.columns
+ # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
+ super.select {|column| column.name.to_s != 'readonly'}
+ end
+
+ def time; Time.at(read_attribute(:time)) end
+ end
+
+ class TracPermission < ActiveRecord::Base
+ set_table_name :permission
+ end
+
+ class TracSessionAttribute < ActiveRecord::Base
+ set_table_name :session_attribute
+ end
+
+ def self.find_or_create_user(username, project_member = false)
+ return User.anonymous if username.blank?
+
+ u = User.find_by_login(username)
+ if !u
+ # Create a new user if not found
+ mail = username[0,limit_for(User, 'mail')]
+ 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 =~ (/(.*)(\s+\w+)?/)
+ fn = $1.strip
+ ln = ($2 || '-').strip
+
+ u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
+ :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
+ :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
+
+ u.login = username[0,limit_for(User, 'login')].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.find(: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 = '<pre>'
+ else
+ m_sl = shebang_re.match(line)
+ if m_sl
+ shebang_line = true
+ line = '<code class="' + m_sl[1] + '">'
+ end
+ m_pre_end = pre_end_re.match(line)
+ if m_pre_end
+ line = '</pre>'
+ if shebang_line
+ line = '</code>' + 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.find(: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.find(: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.find(:all)
+ f.projects << @target_project
+ custom_field_map[field.name] = f
+ end
+ puts
+
+ # Trac 'resolution' field as a Redmine custom field
+ r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
+ r = IssueCustomField.new(:name => 'Resolution',
+ :field_format => 'list',
+ :is_filter => true) if r.nil?
+ r.trackers = Tracker.find(: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.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.find(:all, :order => 'name, version').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)
+ @ic = Iconv.new('UTF-8', charset)
+ rescue Iconv::InvalidEncoding
+ puts "Invalid encoding!"
+ return false
+ end
+
+ def self.set_trac_directory(path)
+ @@trac_directory = path
+ raise "This directory doesn't exist!" unless File.directory?(path)
+ 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(sqlite 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(sqlite 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] "
+ exit if STDIN.gets.match(/^n$/i)
+ end
+ project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
+ project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
+ @target_project = project.new_record? ? nil : project
+ end
+
+ def self.connection_params
+ if %w(sqlite sqlite3).include?(trac_adapter)
+ {:adapter => trac_adapter,
+ :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
+
+ private
+ def self.encode(text)
+ @ic.iconv text
+ rescue
+ text
+ 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] "
+ break unless STDIN.gets.match(/^y$/i)
+ puts
+
+ def prompt(text, options = {}, &block)
+ default = options[:default] || ''
+ while true
+ print "#{text} [#{default}]: "
+ 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 (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
+ unless %w(sqlite 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
+
+ # Turn off email notifications
+ Setting.notified_events = []
+
+ TracMigrate.migrate
+ end
+end
+
--- /dev/null
+namespace :db do\r
+ desc 'Migrates installed plugins.'\r
+ task :migrate_plugins => :environment do\r
+ if Rails.respond_to?('plugins')\r
+ Rails.plugins.each do |plugin|\r
+ next unless plugin.respond_to?('migrate')\r
+ puts "Migrating #{plugin.name}..."\r
+ plugin.migrate\r
+ end\r
+ else\r
+ puts "Undefined method plugins for Rails!"\r
+ puts "Make sure engines plugin is installed."\r
+ end\r
+ end\r
+end\r
--- /dev/null
+require 'source_annotation_extractor'
+
+# Modified version of the SourceAnnotationExtractor in railties
+# Will search for runable code that uses <tt>call_hook</tt>
+class PluginSourceAnnotationExtractor < SourceAnnotationExtractor
+ # Returns a hash that maps filenames under +dir+ (recursively) to arrays
+ # with their annotations. Only files with annotations are included, and only
+ # those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+
+ # are taken into account.
+ def find_in(dir)
+ results = {}
+
+ Dir.glob("#{dir}/*") do |item|
+ next if File.basename(item)[0] == ?.
+
+ if File.directory?(item)
+ results.update(find_in(item))
+ elsif item =~ /(hook|test)\.rb/
+ # skip
+ elsif item =~ /\.(builder|(r(?:b|xml|js)))$/
+ results.update(extract_annotations_from(item, /\s*(#{tag})\(?\s*(.*)$/))
+ elsif item =~ /\.(rhtml|erb)$/
+ results.update(extract_annotations_from(item, /<%=\s*\s*(#{tag})\(?\s*(.*?)\s*%>/))
+ end
+ end
+
+ results
+ end
+end
+
+namespace :redmine do
+ namespace :plugins do
+ desc "Enumerate all Redmine plugin hooks and their context parameters"
+ task :hook_list do
+ PluginSourceAnnotationExtractor.enumerate 'call_hook'
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+desc <<-END_DESC
+Send reminders about issues due in the next days.
+
+Available options:
+ * days => number of days to remind about (defaults to 7)
+ * tracker => id of tracker (defaults to all trackers)
+ * project => id or identifier of project (defaults to all projects)
+
+Example:
+ rake redmine:send_reminders days=7 RAILS_ENV="production"
+END_DESC
+
+namespace :redmine do
+ task :send_reminders => :environment do
+ options = {}
+ options[:days] = ENV['days'].to_i if ENV['days']
+ options[:project] = ENV['project'] if ENV['project']
+ options[:tracker] = ENV['tracker'].to_i if ENV['tracker']
+
+ Mailer.reminders(options)
+ end
+end
--- /dev/null
+### 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"
+ files = Dir.glob("test/**/*_test.rb").join(" ")
+ system("#{rcov} #{files}")
+ system("open coverage/index.html") if PLATFORM['darwin']
+ 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"
+ system "svnadmin create #{repo_path}"
+ system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}"
+ end
+
+ (supported_scms - [:subversion]).each do |scm|
+ desc "Creates a test #{scm} repository"
+ task scm => :create_dir do
+ system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test"
+ end
+ end
+
+ desc "Creates all test repositories"
+ task :all => supported_scms
+ end
+ end
+end
--- /dev/null
+default directory for uploaded files
\ No newline at end of file
--- /dev/null
+# General Apache options\r
+<IfModule mod_fastcgi.c>\r
+ AddHandler fastcgi-script .fcgi\r
+</IfModule>\r
+<IfModule mod_fcgid.c>\r
+ AddHandler fcgid-script .fcgi\r
+</IfModule>\r
+<IfModule mod_cgi.c>\r
+ AddHandler cgi-script .cgi\r
+</IfModule>\r
+Options +FollowSymLinks +ExecCGI\r
+\r
+# If you don't want Rails to look in certain directories,\r
+# use the following rewrite rules so that Apache won't rewrite certain requests\r
+# \r
+# Example:\r
+# RewriteCond %{REQUEST_URI} ^/notrails.*\r
+# RewriteRule .* - [L]\r
+\r
+# Redirect all requests not available on the filesystem to Rails\r
+# By default the cgi dispatcher is used which is very slow\r
+# \r
+# For better performance replace the dispatcher with the fastcgi one\r
+#\r
+# Example:\r
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]\r
+RewriteEngine On\r
+\r
+# If your Rails application is accessed via an Alias directive,\r
+# then you MUST also set the RewriteBase in this htaccess file.\r
+#\r
+# Example:\r
+# Alias /myrailsapp /path/to/myrailsapp/public\r
+# RewriteBase /myrailsapp\r
+\r
+RewriteRule ^$ index.html [QSA]\r
+RewriteRule ^([^.]+)$ $1.html [QSA]\r
+RewriteCond %{REQUEST_FILENAME} !-f\r
+<IfModule mod_fastcgi.c>\r
+ RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]\r
+</IfModule>\r
+<IfModule mod_fcgid.c>\r
+ RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]\r
+</IfModule>\r
+<IfModule mod_cgi.c>\r
+ RewriteRule ^(.*)$ dispatch.cgi [QSA,L]\r
+</IfModule>\r
+\r
+# In case Rails experiences terminal errors\r
+# Instead of displaying this message you can supply a file here which will be rendered instead\r
+# \r
+# Example:\r
+# ErrorDocument 500 /500.html\r
+\r
+ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
\ No newline at end of file
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<title>redMine 404 error</title>
+<style>
+body{
+font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
+color:#303030;
+margin:10px;
+}
+h1{
+font-size:1.5em;
+}
+p{
+font-size:0.8em;
+}
+</style>
+<body>
+ <h1>Page not found</h1>
+ <p>The page you were trying to access doesn't exist or has been removed.</p>
+ <p><a href="javascript:history.back()">Back</a></p>
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<title>redMine 500 error</title>
+<style>
+body{
+font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
+color:#303030;
+margin:10px;
+}
+h1{
+font-size:1.5em;
+}
+p{
+font-size:0.8em;
+}
+</style>
+<body>
+ <h1>Internal error</h1>
+ <p>An error occurred on the page you were trying to access.<br />
+ If you continue to experience problems please contact your redMine administrator for assistance.</p>
+ <p><a href="javascript:history.back()">Back</a></p>
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta http-equiv="Content-Style-Type" content="text/css" />
+<title>Wiki formatting</title>
+<style type="text/css">
+h1 { font-family: Verdana, sans-serif; font-size: 14px; text-align: center; color: #444; }
+body { font-family: Verdana, sans-serif; font-size: 12px; color: #444; }
+table th { padding-top: 1em; }
+table td { vertical-align: top; background-color: #f5f5f5; height: 2em; vertical-align: middle;}
+table td code { font-size: 1.2em; }
+table td h1 { font-size: 1.8em; text-align: left; }
+table td h2 { font-size: 1.4em; text-align: left; }
+table td h3 { font-size: 1.2em; text-align: left; }
+
+</style>
+</head>
+<body>
+
+<h1>Wiki Syntax Quick Reference</h1>
+
+<table width="100%">
+<tr><th colspan="3">Font Styles</th></tr>
+<tr><th><img src="../images/jstoolbar/bt_strong.png" style="border: 1px solid #bbb;" alt="Strong" /></th><td width="50%">*Strong*</td><td width="50%"><strong>Strong</strong></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_em.png" style="border: 1px solid #bbb;" alt="Italic" /></th><td>_Italic_</td><td><em>Italic</em></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_ins.png" style="border: 1px solid #bbb;" alt="Underline" /></th><td>+Underline+</td><td><ins>Underline</ins></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_del.png" style="border: 1px solid #bbb;" alt="Deleted" /></th><td>-Deleted-</td><td><del>Deleted</del></td></tr>
+<tr><th></th><td>??Quote??</td><td><cite>Quote</cite></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_code.png" style="border: 1px solid #bbb;" alt="Inline Code" /></th><td>@Inline Code@</td><td><code>Inline Code</code></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_pre.png" style="border: 1px solid #bbb;" alt="Preformatted text" /></th><td><pre><br /> lines<br /> of code<br /></pre></td><td>
+<pre>
+ lines
+ of code
+</pre>
+</td></tr>
+
+<tr><th colspan="3">Lists</th></tr>
+<tr><th><img src="../images/jstoolbar/bt_ul.png" style="border: 1px solid #bbb;" alt="Unordered list" /></th><td>* Item 1<br />* Item 2</td><td><ul><li>Item 1</li><li>Item 2</li></ul></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_ol.png" style="border: 1px solid #bbb;" alt="Ordered list" /></th><td># Item 1<br /># Item 2</td><td><ol><li>Item 1</li><li>Item 2</li></ol></td></tr>
+
+<tr><th colspan="3">Headings</th></tr>
+<tr><th><img src="../images/jstoolbar/bt_h1.png" style="border: 1px solid #bbb;" alt="Heading 1" /></th><td>h1. Title 1</td><td><h1>Title 1</h1></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_h2.png" style="border: 1px solid #bbb;" alt="Heading 2" /></th><td>h2. Title 2</td><td><h2>Title 2</h2></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_h3.png" style="border: 1px solid #bbb;" alt="Heading 3" /></th><td>h3. Title 3</td><td><h3>Title 3</h3></td></tr>
+
+<tr><th colspan="3">Links</th></tr>
+<tr><th></th><td>http://foo.bar</td><td><a href="#">http://foo.bar</a></td></tr>
+<tr><th></th><td>"Foo":http://foo.bar</td><td><a href="#">Foo</a></td></tr>
+
+<tr><th colspan="3">Redmine links</th></tr>
+<tr><th><img src="../images/jstoolbar/bt_link.png" style="border: 1px solid #bbb;" alt="Link to a Wiki page" /></th><td>[[Wiki page]]</td><td><a href="#">Wiki page</a></td></tr>
+<tr><th></th><td>Issue #12</td><td>Issue <a href="#">#12</a></td></tr>
+<tr><th></th><td>Revision r43</td><td>Revision <a href="#">r43</a></td></tr>
+<tr><th></th><td>commit:f30e13e43</td><td><a href="#">f30e13e4</a></td></tr>
+<tr><th></th><td>source:some/file</td><td><a href="#">source:some/file</a></td></tr>
+
+<tr><th colspan="3">Inline images</th></tr>
+<tr><th><img src="../images/jstoolbar/bt_img.png" style="border: 1px solid #bbb;" alt="Image" /></th><td>!<em>image_url</em>!</td><td></td></tr>
+<tr><th></th><td>!<em>attached_image</em>!</td><td></td></tr>
+</table>
+
+<p><a href="wiki_syntax_detailed.html" onclick="window.open('wiki_syntax_detailed.html', '', ''); return false;">More Information</a></p>
+
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\r
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\r
+<head>\r
+<title>RedmineWikiFormatting</title>\r
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />\r
+<style type="text/css">\r
+ body { font:80% Verdana,Tahoma,Arial,sans-serif; }\r
+ h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }\r
+ pre, code { font-size:120%; }\r
+ pre code { font-size:100%; }\r
+ pre {\r
+ margin: 1em 1em 1em 1.6em;\r
+ padding: 2px;\r
+ background-color: #fafafa;\r
+ border: 1px solid #dadada;\r
+ width:95%;\r
+ overflow-x: auto;\r
+ }\r
+ a.new { color: #b73535; }\r
+\r
+ .CodeRay .c { color:#666; }\r
+ \r
+ .CodeRay .cl { color:#B06; font-weight:bold }\r
+ .CodeRay .dl { color:black }\r
+ .CodeRay .fu { color:#06B; font-weight:bold }\r
+ \r
+ .CodeRay .il { background: #eee }\r
+ .CodeRay .il .idl { font-weight: bold; color: #888 }\r
+ \r
+ .CodeRay .iv { color:#33B }\r
+ .CodeRay .r { color:#080; font-weight:bold }\r
+ \r
+ .CodeRay .s { background-color:#fff0f0 }\r
+ .CodeRay .s .dl { color:#710 }\r
+</style>\r
+</head>\r
+\r
+<body>\r
+<h1><a name="1" class="wiki-page"></a>Wiki formatting</h1>\r
+\r
+ <h2><a name="2" class="wiki-page"></a>Links</h2>\r
+\r
+ <h3><a name="3" class="wiki-page"></a>Redmine links</h3>\r
+\r
+ <p>Redmine allows hyperlinking between issues, changesets and wiki pages from anywhere wiki formatting is used.</p>\r
+ <ul>\r
+ <li>Link to an issue: <strong>#124</strong> (displays <del><a href="/issues/show/124" class="issue" title="bulk edit doesn't change the category or fixed version properties (Closed)">#124</a></del>, link is striked-through if the issue is closed)</li>\r
+ <li>Link to a changeset: <strong>r758</strong> (displays <a href="/repositories/revision/1?rev=758" class="changeset" title="Search engine now only searches objects the user is allowed to view.">r758</a>)</li>\r
+ <li>Link to a changeset with a non-numeric hash: <strong>commit:c6f4d0fd</strong> (displays c6f4d0fd). Added in <a href="/repositories/revision/1?rev=1236" class="changeset" title="Merged Git support branch (r1200 to r1226).">r1236</a>.</li>\r
+ </ul>\r
+\r
+ <p>Wiki links:</p>\r
+\r
+ <ul>\r
+ <li><strong>[[Guide]]</strong> displays a link to the page named 'Guide': <a href="Guide.html" class="wiki-page">Guide</a></li>\r
+ <li><strong>[[Guide#further-reading]]</strong> takes you to the anchor "further-reading". Headings get automatically assigned anchors so that you can refer to them: <a href="Guide.html#further-reading" class="wiki-page">Guide</a></li>\r
+ <li><strong>[[Guide|User manual]]</strong> displays a link to the same page but with a different text: <a href="Guide.html" class="wiki-page">User manual</a></li>\r
+ </ul>\r
+\r
+ <p>You can also link to pages of an other project wiki:</p>\r
+\r
+ <ul>\r
+ <li><strong>[[sandbox:some page]]</strong> displays a link to the page named 'Some page' of the Sandbox wiki</li>\r
+ <li><strong>[[sandbox:]]</strong> displays a link to the Sandbox wiki main page</li>\r
+ </ul>\r
+\r
+ <p>Wiki links are displayed in red if the page doesn't exist yet, eg: <a href="Nonexistent_page.html" class="wiki-page new">Nonexistent page</a>.</p>\r
+\r
+ <p>Links to others resources (0.7):</p>\r
+\r
+ <ul>\r
+ <li>Documents:\r
+ <ul>\r
+ <li><strong>document#17</strong> (link to document with id 17)</li>\r
+ <li><strong>document:Greetings</strong> (link to the document with title "Greetings")</li>\r
+ <li><strong>document:"Some document"</strong> (double quotes can be used when document title contains spaces)</li>\r
+ </ul></li>\r
+ </ul>\r
+\r
+ <ul>\r
+ <li>Versions:\r
+ <ul>\r
+ <li><strong>version#3</strong> (link to version with id 3)</li>\r
+ <li><strong>version:1.0.0</strong> (link to version named "1.0.0")</li>\r
+ <li><strong>version:"1.0 beta 2"</strong></li>\r
+ </ul></li>\r
+ </ul>\r
+\r
+ <ul>\r
+ <li>Attachments:\r
+ <ul>\r
+ <li><strong>attachment:file.zip</strong> (link to the attachment of the current object named file.zip)</li>\r
+ <li>For now, attachments of the current object can be referenced only (if you're on an issue, it's possible to reference attachments of this issue only)</li>\r
+ </ul></li>\r
+ </ul>\r
+\r
+ <ul>\r
+ <li>Repository files\r
+ <ul>\r
+ <li><strong>source:some/file</strong> -- Link to the file located at /some/file in the project's repository</li>\r
+ <li><strong>source:some/file@52</strong> -- Link to the file's revision 52</li>\r
+ <li><strong>source:some/file#L120</strong> -- Link to line 120 of the file</li>\r
+ <li><strong>source:some/file@52#L120</strong> -- Link to line 120 of the file's revision 52</li>\r
+ <li><strong>export:some/file</strong> -- Force the download of the file</li>\r
+ </ul></li>\r
+ </ul>\r
+ \r
+ <p>Escaping (0.7):</p>\r
+\r
+ <ul>\r
+ <li>You can prevent Redmine links from being parsed by preceding them with an exclamation mark: !</li>\r
+ </ul>\r
+\r
+\r
+ <h3><a name="4" class="wiki-page"></a>External links</h3>\r
+\r
+ <p>HTTP URLs and email addresses are automatically turned into clickable links:</p>\r
+\r
+<pre>\r
+http://www.redmine.org, someone@foo.bar\r
+</pre>\r
+\r
+ <p>displays: <a class="external" href="http://www.redmine.org">http://www.redmine.org</a>, <a href="mailto:someone@foo.bar" class="email">someone@foo.bar</a></p>\r
+\r
+ <p>If you want to display a specific text instead of the URL, you can use the standard textile syntax:</p>\r
+\r
+<pre>\r
+"Redmine web site":http://www.redmine.org\r
+</pre>\r
+\r
+ <p>displays: <a href="http://www.redmine.org" class="external">Redmine web site</a></p>\r
+\r
+\r
+ <h2><a name="5" class="wiki-page"></a>Text formatting</h2>\r
+\r
+\r
+ <p>For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See <a class="external" href="http://www.textism.com/tools/textile/">http://www.textism.com/tools/textile/</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p>\r
+\r
+ <h3><a name="6" class="wiki-page"></a>Font style</h3>\r
+\r
+<pre>\r
+* *bold*\r
+* _italic_\r
+* _*bold italic*_\r
+* +underline+\r
+* -strike-through-\r
+</pre>\r
+\r
+ <p>Display:</p>\r
+\r
+ <ul>\r
+ <li><strong>bold</strong></li>\r
+ <li><em>italic</em></li>\r
+ <li><em>*bold italic*</em></li>\r
+ <li><ins>underline</ins></li>\r
+ <li><del>strike-through</del></li>\r
+ </ul>\r
+\r
+ <h3><a name="7" class="wiki-page"></a>Inline images</h3>\r
+\r
+ <ul>\r
+ <li><strong>!image_url!</strong> displays an image located at image_url (textile syntax)</li>\r
+ <li><strong>!>image_url!</strong> right floating image</li>\r
+ <li>If you have an image attached to your wiki page, it can be displayed inline using its filename: <strong>!attached_image.png!</strong></li>\r
+ </ul>\r
+\r
+ <h3><a name="8" class="wiki-page"></a>Headings</h3>\r
+\r
+<pre>\r
+h1. Heading\r
+h2. Subheading\r
+h3. Subsubheading\r
+</pre>\r
+\r
+ <p>Redmine assigns an anchor to each of those headings thus you can link to them with "#Heading", "#Subheading" and so forth.</p>\r
+\r
+\r
+ <h3><a name="9" class="wiki-page"></a>Paragraphs</h3>\r
+\r
+<pre>\r
+p>. right aligned\r
+p=. centered\r
+</pre>\r
+\r
+ <p style="text-align:center;">This is centered paragraph.</p>\r
+\r
+\r
+ <h3><a name="10" class="wiki-page"></a>Blockquotes</h3>\r
+\r
+ <p>Start the paragraph with <strong>bq.</strong></p>\r
+\r
+<pre>\r
+bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.\r
+To go live, all you need to add is a database and a web server.\r
+</pre>\r
+\r
+ <p>Display:</p>\r
+\r
+ <blockquote>\r
+ <p>Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.<br />To go live, all you need to add is a database and a web server.</p>\r
+ </blockquote>\r
+\r
+\r
+ <h3><a name="11" class="wiki-page"></a>Table of content</h3>\r
+\r
+<pre>\r
+{{toc}} => left aligned toc\r
+{{>toc}} => right aligned toc\r
+</pre>\r
+\r
+ <h2><a name="12" class="wiki-page"></a>Macros</h2>\r
+\r
+ <p>Redmine has the following builtin macros:</p>\r
+\r
+ <p><dl><dt><code>hello_world</code></dt><dd><p>Sample macro.</p></dd><dt><code>include</code></dt><dd><p>Include a wiki page. Example:</p>\r
+\r
+ <pre><code>{{include(Foo)}}</code></pre></dd><dt><code>macro_list</code></dt><dd><p>Displays a list of all available macros, including description if available.</p></dd></dl></p>\r
+\r
+\r
+ <h2><a name="13" class="wiki-page"></a>Code highlighting</h2>\r
+\r
+ <p>Code highlightment relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, html, javascript, rhtml, ruby, scheme, xml languages.</p>\r
+\r
+ <p>You can highlight code in your wiki page using this syntax:</p>\r
+\r
+<pre>\r
+<pre><code class="ruby">\r
+ Place you code here.\r
+</code></pre>\r
+</pre>\r
+\r
+ <p>Example:</p>\r
+\r
+<pre><code class="ruby CodeRay"><span class="no"> 1</span> <span class="c"># The Greeter class</span>\r
+<span class="no"> 2</span> <span class="r">class</span> <span class="cl">Greeter</span>\r
+<span class="no"> 3</span> <span class="r">def</span> <span class="fu">initialize</span>(name)\r
+<span class="no"> 4</span> <span class="iv">@name</span> = name.capitalize\r
+<span class="no"> 5</span> <span class="r">end</span>\r
+<span class="no"> 6</span> \r
+<span class="no"> 7</span> <span class="r">def</span> <span class="fu">salute</span>\r
+<span class="no"> 8</span> puts <span class="s"><span class="dl">"</span><span class="k">Hello </span><span class="il"><span class="idl">#{</span><span class="iv">@name</span><span class="idl">}</span></span><span class="k">!</span><span class="dl">"</span></span> \r
+<span class="no"> 9</span> <span class="r">end</span>\r
+<span class="no"><strong>10</strong></span> <span class="r">end</span>\r
+</code>\r
+</pre>\r
+</body>\r
+</html>\r
--- /dev/null
+/* redMine - project management software
+ Copyright (C) 2006-2008 Jean-Philippe Lang */
+
+function checkAll (id, checked) {
+ var els = Element.descendants(id);
+ for (var i = 0; i < els.length; i++) {
+ if (els[i].disabled==false) {
+ els[i].checked = checked;
+ }
+ }
+}
+
+function toggleCheckboxesBySelector(selector) {
+ boxes = $$(selector);
+ var all_checked = true;
+ for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
+ for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
+}
+
+function showAndScrollTo(id, focus) {
+ Element.show(id);
+ if (focus!=null) { Form.Element.focus(focus); }
+ Element.scrollTo(id);
+}
+
+function toggleRowGroup(el) {
+ var tr = Element.up(el, 'tr');
+ var n = Element.next(tr);
+ tr.toggleClassName('open');
+ while (n != undefined && !n.hasClassName('group')) {
+ Element.toggle(n);
+ n = Element.next(n);
+ }
+}
+
+function toggleFieldset(el) {
+ var fieldset = Element.up(el, 'fieldset');
+ fieldset.toggleClassName('collapsed');
+ Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
+}
+
+var fileFieldCount = 1;
+
+function addFileField() {
+ if (fileFieldCount >= 10) return false
+ fileFieldCount++;
+ var f = document.createElement("input");
+ f.type = "file";
+ f.name = "attachments[" + fileFieldCount + "][file]";
+ f.size = 30;
+ var d = document.createElement("input");
+ d.type = "text";
+ d.name = "attachments[" + fileFieldCount + "][description]";
+ d.size = 60;
+
+ p = document.getElementById("attachments_fields");
+ p.appendChild(document.createElement("br"));
+ p.appendChild(f);
+ p.appendChild(d);
+}
+
+function showTab(name) {
+ var f = $$('div#content .tab-content');
+ for(var i=0; i<f.length; i++){
+ Element.hide(f[i]);
+ }
+ var f = $$('div.tabs a');
+ for(var i=0; i<f.length; i++){
+ Element.removeClassName(f[i], "selected");
+ }
+ Element.show('tab-content-' + name);
+ Element.addClassName('tab-' + name, "selected");
+ return false;
+}
+
+function setPredecessorFieldsVisibility() {
+ relationType = $('relation_relation_type');
+ if (relationType && relationType.value == "precedes") {
+ Element.show('predecessor_fields');
+ } else {
+ Element.hide('predecessor_fields');
+ }
+}
+
+function promptToRemote(text, param, url) {
+ value = prompt(text + ':');
+ if (value) {
+ new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
+ return false;
+ }
+}
+
+function collapseScmEntry(id) {
+ var els = document.getElementsByClassName(id, 'browser');
+ for (var i = 0; i < els.length; i++) {
+ if (els[i].hasClassName('open')) {
+ collapseScmEntry(els[i].id);
+ }
+ Element.hide(els[i]);
+ }
+ $(id).removeClassName('open');
+}
+
+function expandScmEntry(id) {
+ var els = document.getElementsByClassName(id, 'browser');
+ for (var i = 0; i < els.length; i++) {
+ Element.show(els[i]);
+ if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
+ expandScmEntry(els[i].id);
+ }
+ }
+ $(id).addClassName('open');
+}
+
+function scmEntryClick(id) {
+ el = $(id);
+ if (el.hasClassName('open')) {
+ collapseScmEntry(id);
+ el.addClassName('collapsed');
+ return false;
+ } else if (el.hasClassName('loaded')) {
+ expandScmEntry(id);
+ el.removeClassName('collapsed');
+ return false;
+ }
+ if (el.hasClassName('loading')) {
+ return false;
+ }
+ el.addClassName('loading');
+ return true;
+}
+
+function scmEntryLoaded(id) {
+ Element.addClassName(id, 'open');
+ Element.addClassName(id, 'loaded');
+ Element.removeClassName(id, 'loading');
+}
+
+function randomKey(size) {
+ var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
+ var key = '';
+ for (i = 0; i < size; i++) {
+ key += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return key;
+}
+
+/* shows and hides ajax indicator */
+Ajax.Responders.register({
+ onCreate: function(){
+ if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
+ Element.show('ajax-indicator');
+ }
+ },
+ onComplete: function(){
+ if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
+ Element.hide('ajax-indicator');
+ }
+ }
+});
--- /dev/null
+/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/
+ * ---------------------------------------------------------------------------
+ *
+ * The DHTML Calendar
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ *
+ * This file defines helper functions for setting up the calendar. They are
+ * intended to help non-programmers get a working calendar on their site
+ * quickly. This script should not be seen as part of the calendar. It just
+ * shows you what one can do with the calendar, while in the same time
+ * providing a quick and simple method for setting it up. If you need
+ * exhaustive customization of the calendar creation process feel free to
+ * modify this code to suit your needs (this is recommended and much better
+ * than modifying calendar.js itself).
+ */
+
+// $Id: calendar-setup.js,v 1.25 2005/03/07 09:51:33 mishoo Exp $
+
+/**
+ * This function "patches" an input field (or other element) to use a calendar
+ * widget for date selection.
+ *
+ * The "params" is a single object that can have the following properties:
+ *
+ * prop. name | description
+ * -------------------------------------------------------------------------------------------------
+ * inputField | the ID of an input field to store the date
+ * displayArea | the ID of a DIV or other element to show the date
+ * button | ID of a button or other element that will trigger the calendar
+ * eventName | event that will trigger the calendar, without the "on" prefix (default: "click")
+ * ifFormat | date format that will be stored in the input field
+ * daFormat | the date format that will be used to display the date in displayArea
+ * singleClick | (true/false) wether the calendar is in single click mode or not (default: true)
+ * firstDay | numeric: 0 to 6. "0" means display Sunday first, "1" means display Monday first, etc.
+ * align | alignment (default: "Br"); if you don't know what's this see the calendar documentation
+ * range | array with 2 elements. Default: [1900, 2999] -- the range of years available
+ * weekNumbers | (true/false) if it's true (default) the calendar will display week numbers
+ * flat | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID
+ * flatCallback | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar)
+ * disableFunc | function that receives a JS Date object and should return true if that date has to be disabled in the calendar
+ * onSelect | function that gets called when a date is selected. You don't _have_ to supply this (the default is generally okay)
+ * onClose | function that gets called when the calendar is closed. [default]
+ * onUpdate | function that gets called after the date is updated in the input field. Receives a reference to the calendar.
+ * date | the date that the calendar will be initially displayed to
+ * showsTime | default: false; if true the calendar will include a time selector
+ * timeFormat | the time format; can be "12" or "24", default is "12"
+ * electric | if true (default) then given fields/date areas are updated for each move; otherwise they're updated only on close
+ * step | configures the step of the years in drop-down boxes; default: 2
+ * position | configures the calendar absolute position; default: null
+ * cache | if "true" (but default: "false") it will reuse the same calendar object, where possible
+ * showOthers | if "true" (but default: "false") it will show days from other months too
+ *
+ * None of them is required, they all have default values. However, if you
+ * pass none of "inputField", "displayArea" or "button" you'll get a warning
+ * saying "nothing to setup".
+ */
+Calendar.setup = function (params) {
+ function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } };
+
+ param_default("inputField", null);
+ param_default("displayArea", null);
+ param_default("button", null);
+ param_default("eventName", "click");
+ param_default("ifFormat", "%Y/%m/%d");
+ param_default("daFormat", "%Y/%m/%d");
+ param_default("singleClick", true);
+ param_default("disableFunc", null);
+ param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined
+ param_default("dateText", null);
+ param_default("firstDay", null);
+ param_default("align", "Br");
+ param_default("range", [1900, 2999]);
+ param_default("weekNumbers", true);
+ param_default("flat", null);
+ param_default("flatCallback", null);
+ param_default("onSelect", null);
+ param_default("onClose", null);
+ param_default("onUpdate", null);
+ param_default("date", null);
+ param_default("showsTime", false);
+ param_default("timeFormat", "24");
+ param_default("electric", true);
+ param_default("step", 2);
+ param_default("position", null);
+ param_default("cache", false);
+ param_default("showOthers", false);
+ param_default("multiple", null);
+
+ var tmp = ["inputField", "displayArea", "button"];
+ for (var i in tmp) {
+ if (typeof params[tmp[i]] == "string") {
+ params[tmp[i]] = document.getElementById(params[tmp[i]]);
+ }
+ }
+ if (!(params.flat || params.multiple || params.inputField || params.displayArea || params.button)) {
+ alert("Calendar.setup:\n Nothing to setup (no fields found). Please check your code");
+ return false;
+ }
+
+ function onSelect(cal) {
+ var p = cal.params;
+ var update = (cal.dateClicked || p.electric);
+ if (update && p.inputField) {
+ p.inputField.value = cal.date.print(p.ifFormat);
+ if (typeof p.inputField.onchange == "function")
+ p.inputField.onchange();
+ }
+ if (update && p.displayArea)
+ p.displayArea.innerHTML = cal.date.print(p.daFormat);
+ if (update && typeof p.onUpdate == "function")
+ p.onUpdate(cal);
+ if (update && p.flat) {
+ if (typeof p.flatCallback == "function")
+ p.flatCallback(cal);
+ }
+ if (update && p.singleClick && cal.dateClicked)
+ cal.callCloseHandler();
+ };
+
+ if (params.flat != null) {
+ if (typeof params.flat == "string")
+ params.flat = document.getElementById(params.flat);
+ if (!params.flat) {
+ alert("Calendar.setup:\n Flat specified but can't find parent.");
+ return false;
+ }
+ var cal = new Calendar(params.firstDay, params.date, params.onSelect || onSelect);
+ cal.showsOtherMonths = params.showOthers;
+ cal.showsTime = params.showsTime;
+ cal.time24 = (params.timeFormat == "24");
+ cal.params = params;
+ cal.weekNumbers = params.weekNumbers;
+ cal.setRange(params.range[0], params.range[1]);
+ cal.setDateStatusHandler(params.dateStatusFunc);
+ cal.getDateText = params.dateText;
+ if (params.ifFormat) {
+ cal.setDateFormat(params.ifFormat);
+ }
+ if (params.inputField && typeof params.inputField.value == "string") {
+ cal.parseDate(params.inputField.value);
+ }
+ cal.create(params.flat);
+ cal.show();
+ return false;
+ }
+
+ var triggerEl = params.button || params.displayArea || params.inputField;
+ triggerEl["on" + params.eventName] = function() {
+ var dateEl = params.inputField || params.displayArea;
+ var dateFmt = params.inputField ? params.ifFormat : params.daFormat;
+ var mustCreate = false;
+ var cal = window.calendar;
+ if (dateEl)
+ params.date = Date.parseDate(dateEl.value || dateEl.innerHTML, dateFmt);
+ if (!(cal && params.cache)) {
+ window.calendar = cal = new Calendar(params.firstDay,
+ params.date,
+ params.onSelect || onSelect,
+ params.onClose || function(cal) { cal.hide(); });
+ cal.showsTime = params.showsTime;
+ cal.time24 = (params.timeFormat == "24");
+ cal.weekNumbers = params.weekNumbers;
+ mustCreate = true;
+ } else {
+ if (params.date)
+ cal.setDate(params.date);
+ cal.hide();
+ }
+ if (params.multiple) {
+ cal.multiple = {};
+ for (var i = params.multiple.length; --i >= 0;) {
+ var d = params.multiple[i];
+ var ds = d.print("%Y%m%d");
+ cal.multiple[ds] = d;
+ }
+ }
+ cal.showsOtherMonths = params.showOthers;
+ cal.yearStep = params.step;
+ cal.setRange(params.range[0], params.range[1]);
+ cal.params = params;
+ cal.setDateStatusHandler(params.dateStatusFunc);
+ cal.getDateText = params.dateText;
+ cal.setDateFormat(dateFmt);
+ if (mustCreate)
+ cal.create();
+ cal.refresh();
+ if (!params.position)
+ cal.showAtElement(params.button || params.displayArea || params.inputField, params.align);
+ else
+ cal.showAt(params.position[0], params.position[1]);
+ return false;
+ };
+
+ return cal;
+};
--- /dev/null
+/* Copyright Mihai Bazon, 2002-2005 | www.bazon.net/mishoo
+ * -----------------------------------------------------------
+ *
+ * The DHTML Calendar, version 1.0 "It is happening again"
+ *
+ * Details and latest version at:
+ * www.dynarch.com/projects/calendar
+ *
+ * This script is developed by Dynarch.com. Visit us at www.dynarch.com.
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ */
+
+// $Id: calendar.js,v 1.51 2005/03/07 16:44:31 mishoo Exp $
+
+/** The Calendar object constructor. */
+Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) {
+ // member variables
+ this.activeDiv = null;
+ this.currentDateEl = null;
+ this.getDateStatus = null;
+ this.getDateToolTip = null;
+ this.getDateText = null;
+ this.timeout = null;
+ this.onSelected = onSelected || null;
+ this.onClose = onClose || null;
+ this.dragging = false;
+ this.hidden = false;
+ this.minYear = 1970;
+ this.maxYear = 2050;
+ this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"];
+ this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"];
+ this.isPopup = true;
+ this.weekNumbers = true;
+ this.firstDayOfWeek = typeof firstDayOfWeek == "number" ? firstDayOfWeek : Calendar._FD; // 0 for Sunday, 1 for Monday, etc.
+ this.showsOtherMonths = false;
+ this.dateStr = dateStr;
+ this.ar_days = null;
+ this.showsTime = false;
+ this.time24 = true;
+ this.yearStep = 2;
+ this.hiliteToday = true;
+ this.multiple = null;
+ // HTML elements
+ this.table = null;
+ this.element = null;
+ this.tbody = null;
+ this.firstdayname = null;
+ // Combo boxes
+ this.monthsCombo = null;
+ this.yearsCombo = null;
+ this.hilitedMonth = null;
+ this.activeMonth = null;
+ this.hilitedYear = null;
+ this.activeYear = null;
+ // Information
+ this.dateClicked = false;
+
+ // one-time initializations
+ if (typeof Calendar._SDN == "undefined") {
+ // table of short day names
+ if (typeof Calendar._SDN_len == "undefined")
+ Calendar._SDN_len = 3;
+ var ar = new Array();
+ for (var i = 8; i > 0;) {
+ ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len);
+ }
+ Calendar._SDN = ar;
+ // table of short month names
+ if (typeof Calendar._SMN_len == "undefined")
+ Calendar._SMN_len = 3;
+ ar = new Array();
+ for (var i = 12; i > 0;) {
+ ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len);
+ }
+ Calendar._SMN = ar;
+ }
+};
+
+// ** constants
+
+/// "static", needed for event handlers.
+Calendar._C = null;
+
+/// detect a special case of "web browser"
+Calendar.is_ie = ( /msie/i.test(navigator.userAgent) &&
+ !/opera/i.test(navigator.userAgent) );
+
+Calendar.is_ie5 = ( Calendar.is_ie && /msie 5\.0/i.test(navigator.userAgent) );
+
+/// detect Opera browser
+Calendar.is_opera = /opera/i.test(navigator.userAgent);
+
+/// detect KHTML-based browsers
+Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent);
+
+// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate
+// library, at some point.
+
+Calendar.getAbsolutePos = function(el) {
+ var SL = 0, ST = 0;
+ var is_div = /^div$/i.test(el.tagName);
+ if (is_div && el.scrollLeft)
+ SL = el.scrollLeft;
+ if (is_div && el.scrollTop)
+ ST = el.scrollTop;
+ var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
+ if (el.offsetParent) {
+ var tmp = this.getAbsolutePos(el.offsetParent);
+ r.x += tmp.x;
+ r.y += tmp.y;
+ }
+ return r;
+};
+
+Calendar.isRelated = function (el, evt) {
+ var related = evt.relatedTarget;
+ if (!related) {
+ var type = evt.type;
+ if (type == "mouseover") {
+ related = evt.fromElement;
+ } else if (type == "mouseout") {
+ related = evt.toElement;
+ }
+ }
+ while (related) {
+ if (related == el) {
+ return true;
+ }
+ related = related.parentNode;
+ }
+ return false;
+};
+
+Calendar.removeClass = function(el, className) {
+ if (!(el && el.className)) {
+ return;
+ }
+ var cls = el.className.split(" ");
+ var ar = new Array();
+ for (var i = cls.length; i > 0;) {
+ if (cls[--i] != className) {
+ ar[ar.length] = cls[i];
+ }
+ }
+ el.className = ar.join(" ");
+};
+
+Calendar.addClass = function(el, className) {
+ Calendar.removeClass(el, className);
+ el.className += " " + className;
+};
+
+// FIXME: the following 2 functions totally suck, are useless and should be replaced immediately.
+Calendar.getElement = function(ev) {
+ var f = Calendar.is_ie ? window.event.srcElement : ev.currentTarget;
+ while (f.nodeType != 1 || /^div$/i.test(f.tagName))
+ f = f.parentNode;
+ return f;
+};
+
+Calendar.getTargetElement = function(ev) {
+ var f = Calendar.is_ie ? window.event.srcElement : ev.target;
+ while (f.nodeType != 1)
+ f = f.parentNode;
+ return f;
+};
+
+Calendar.stopEvent = function(ev) {
+ ev || (ev = window.event);
+ if (Calendar.is_ie) {
+ ev.cancelBubble = true;
+ ev.returnValue = false;
+ } else {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ return false;
+};
+
+Calendar.addEvent = function(el, evname, func) {
+ if (el.attachEvent) { // IE
+ el.attachEvent("on" + evname, func);
+ } else if (el.addEventListener) { // Gecko / W3C
+ el.addEventListener(evname, func, true);
+ } else {
+ el["on" + evname] = func;
+ }
+};
+
+Calendar.removeEvent = function(el, evname, func) {
+ if (el.detachEvent) { // IE
+ el.detachEvent("on" + evname, func);
+ } else if (el.removeEventListener) { // Gecko / W3C
+ el.removeEventListener(evname, func, true);
+ } else {
+ el["on" + evname] = null;
+ }
+};
+
+Calendar.createElement = function(type, parent) {
+ var el = null;
+ if (document.createElementNS) {
+ // use the XHTML namespace; IE won't normally get here unless
+ // _they_ "fix" the DOM2 implementation.
+ el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
+ } else {
+ el = document.createElement(type);
+ }
+ if (typeof parent != "undefined") {
+ parent.appendChild(el);
+ }
+ return el;
+};
+
+// END: UTILITY FUNCTIONS
+
+// BEGIN: CALENDAR STATIC FUNCTIONS
+
+/** Internal -- adds a set of events to make some element behave like a button. */
+Calendar._add_evs = function(el) {
+ with (Calendar) {
+ addEvent(el, "mouseover", dayMouseOver);
+ addEvent(el, "mousedown", dayMouseDown);
+ addEvent(el, "mouseout", dayMouseOut);
+ if (is_ie) {
+ addEvent(el, "dblclick", dayMouseDblClick);
+ el.setAttribute("unselectable", true);
+ }
+ }
+};
+
+Calendar.findMonth = function(el) {
+ if (typeof el.month != "undefined") {
+ return el;
+ } else if (typeof el.parentNode.month != "undefined") {
+ return el.parentNode;
+ }
+ return null;
+};
+
+Calendar.findYear = function(el) {
+ if (typeof el.year != "undefined") {
+ return el;
+ } else if (typeof el.parentNode.year != "undefined") {
+ return el.parentNode;
+ }
+ return null;
+};
+
+Calendar.showMonthsCombo = function () {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ var cal = cal;
+ var cd = cal.activeDiv;
+ var mc = cal.monthsCombo;
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ if (cal.activeMonth) {
+ Calendar.removeClass(cal.activeMonth, "active");
+ }
+ var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];
+ Calendar.addClass(mon, "active");
+ cal.activeMonth = mon;
+ var s = mc.style;
+ s.display = "block";
+ if (cd.navtype < 0)
+ s.left = cd.offsetLeft + "px";
+ else {
+ var mcw = mc.offsetWidth;
+ if (typeof mcw == "undefined")
+ // Konqueror brain-dead techniques
+ mcw = 50;
+ s.left = (cd.offsetLeft + cd.offsetWidth - mcw) + "px";
+ }
+ s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+};
+
+Calendar.showYearsCombo = function (fwd) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ var cal = cal;
+ var cd = cal.activeDiv;
+ var yc = cal.yearsCombo;
+ if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ if (cal.activeYear) {
+ Calendar.removeClass(cal.activeYear, "active");
+ }
+ cal.activeYear = null;
+ var Y = cal.date.getFullYear() + (fwd ? 1 : -1);
+ var yr = yc.firstChild;
+ var show = false;
+ for (var i = 12; i > 0; --i) {
+ if (Y >= cal.minYear && Y <= cal.maxYear) {
+ yr.innerHTML = Y;
+ yr.year = Y;
+ yr.style.display = "block";
+ show = true;
+ } else {
+ yr.style.display = "none";
+ }
+ yr = yr.nextSibling;
+ Y += fwd ? cal.yearStep : -cal.yearStep;
+ }
+ if (show) {
+ var s = yc.style;
+ s.display = "block";
+ if (cd.navtype < 0)
+ s.left = cd.offsetLeft + "px";
+ else {
+ var ycw = yc.offsetWidth;
+ if (typeof ycw == "undefined")
+ // Konqueror brain-dead techniques
+ ycw = 50;
+ s.left = (cd.offsetLeft + cd.offsetWidth - ycw) + "px";
+ }
+ s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+ }
+};
+
+// event handlers
+
+Calendar.tableMouseUp = function(ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ if (cal.timeout) {
+ clearTimeout(cal.timeout);
+ }
+ var el = cal.activeDiv;
+ if (!el) {
+ return false;
+ }
+ var target = Calendar.getTargetElement(ev);
+ ev || (ev = window.event);
+ Calendar.removeClass(el, "active");
+ if (target == el || target.parentNode == el) {
+ Calendar.cellClick(el, ev);
+ }
+ var mon = Calendar.findMonth(target);
+ var date = null;
+ if (mon) {
+ date = new Date(cal.date);
+ if (mon.month != date.getMonth()) {
+ date.setMonth(mon.month);
+ cal.setDate(date);
+ cal.dateClicked = false;
+ cal.callHandler();
+ }
+ } else {
+ var year = Calendar.findYear(target);
+ if (year) {
+ date = new Date(cal.date);
+ if (year.year != date.getFullYear()) {
+ date.setFullYear(year.year);
+ cal.setDate(date);
+ cal.dateClicked = false;
+ cal.callHandler();
+ }
+ }
+ }
+ with (Calendar) {
+ removeEvent(document, "mouseup", tableMouseUp);
+ removeEvent(document, "mouseover", tableMouseOver);
+ removeEvent(document, "mousemove", tableMouseOver);
+ cal._hideCombos();
+ _C = null;
+ return stopEvent(ev);
+ }
+};
+
+Calendar.tableMouseOver = function (ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return;
+ }
+ var el = cal.activeDiv;
+ var target = Calendar.getTargetElement(ev);
+ if (target == el || target.parentNode == el) {
+ Calendar.addClass(el, "hilite active");
+ Calendar.addClass(el.parentNode, "rowhilite");
+ } else {
+ if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2)))
+ Calendar.removeClass(el, "active");
+ Calendar.removeClass(el, "hilite");
+ Calendar.removeClass(el.parentNode, "rowhilite");
+ }
+ ev || (ev = window.event);
+ if (el.navtype == 50 && target != el) {
+ var pos = Calendar.getAbsolutePos(el);
+ var w = el.offsetWidth;
+ var x = ev.clientX;
+ var dx;
+ var decrease = true;
+ if (x > pos.x + w) {
+ dx = x - pos.x - w;
+ decrease = false;
+ } else
+ dx = pos.x - x;
+
+ if (dx < 0) dx = 0;
+ var range = el._range;
+ var current = el._current;
+ var count = Math.floor(dx / 10) % range.length;
+ for (var i = range.length; --i >= 0;)
+ if (range[i] == current)
+ break;
+ while (count-- > 0)
+ if (decrease) {
+ if (--i < 0)
+ i = range.length - 1;
+ } else if ( ++i >= range.length )
+ i = 0;
+ var newval = range[i];
+ el.innerHTML = newval;
+
+ cal.onUpdateTime();
+ }
+ var mon = Calendar.findMonth(target);
+ if (mon) {
+ if (mon.month != cal.date.getMonth()) {
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ Calendar.addClass(mon, "hilite");
+ cal.hilitedMonth = mon;
+ } else if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ } else {
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ var year = Calendar.findYear(target);
+ if (year) {
+ if (year.year != cal.date.getFullYear()) {
+ if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ Calendar.addClass(year, "hilite");
+ cal.hilitedYear = year;
+ } else if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ } else if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.tableMouseDown = function (ev) {
+ if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) {
+ return Calendar.stopEvent(ev);
+ }
+};
+
+Calendar.calDragIt = function (ev) {
+ var cal = Calendar._C;
+ if (!(cal && cal.dragging)) {
+ return false;
+ }
+ var posX;
+ var posY;
+ if (Calendar.is_ie) {
+ posY = window.event.clientY + document.body.scrollTop;
+ posX = window.event.clientX + document.body.scrollLeft;
+ } else {
+ posX = ev.pageX;
+ posY = ev.pageY;
+ }
+ cal.hideShowCovered();
+ var st = cal.element.style;
+ st.left = (posX - cal.xOffs) + "px";
+ st.top = (posY - cal.yOffs) + "px";
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.calDragEnd = function (ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ cal.dragging = false;
+ with (Calendar) {
+ removeEvent(document, "mousemove", calDragIt);
+ removeEvent(document, "mouseup", calDragEnd);
+ tableMouseUp(ev);
+ }
+ cal.hideShowCovered();
+};
+
+Calendar.dayMouseDown = function(ev) {
+ var el = Calendar.getElement(ev);
+ if (el.disabled) {
+ return false;
+ }
+ var cal = el.calendar;
+ cal.activeDiv = el;
+ Calendar._C = cal;
+ if (el.navtype != 300) with (Calendar) {
+ if (el.navtype == 50) {
+ el._current = el.innerHTML;
+ addEvent(document, "mousemove", tableMouseOver);
+ } else
+ addEvent(document, Calendar.is_ie5 ? "mousemove" : "mouseover", tableMouseOver);
+ addClass(el, "hilite active");
+ addEvent(document, "mouseup", tableMouseUp);
+ } else if (cal.isPopup) {
+ cal._dragStart(ev);
+ }
+ if (el.navtype == -1 || el.navtype == 1) {
+ if (cal.timeout) clearTimeout(cal.timeout);
+ cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250);
+ } else if (el.navtype == -2 || el.navtype == 2) {
+ if (cal.timeout) clearTimeout(cal.timeout);
+ cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250);
+ } else {
+ cal.timeout = null;
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseDblClick = function(ev) {
+ Calendar.cellClick(Calendar.getElement(ev), ev || window.event);
+ if (Calendar.is_ie) {
+ document.selection.empty();
+ }
+};
+
+Calendar.dayMouseOver = function(ev) {
+ var el = Calendar.getElement(ev);
+ if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) {
+ return false;
+ }
+ if (el.ttip) {
+ if (el.ttip.substr(0, 1) == "_") {
+ el.ttip = el.caldate.print(el.calendar.ttDateFormat) + el.ttip.substr(1);
+ }
+ el.calendar.tooltips.innerHTML = el.ttip;
+ }
+ if (el.navtype != 300) {
+ Calendar.addClass(el, "hilite");
+ if (el.caldate) {
+ Calendar.addClass(el.parentNode, "rowhilite");
+ }
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseOut = function(ev) {
+ with (Calendar) {
+ var el = getElement(ev);
+ if (isRelated(el, ev) || _C || el.disabled)
+ return false;
+ removeClass(el, "hilite");
+ if (el.caldate)
+ removeClass(el.parentNode, "rowhilite");
+ if (el.calendar)
+ el.calendar.tooltips.innerHTML = _TT["SEL_DATE"];
+ return stopEvent(ev);
+ }
+};
+
+/**
+ * A generic "click" handler :) handles all types of buttons defined in this
+ * calendar.
+ */
+Calendar.cellClick = function(el, ev) {
+ var cal = el.calendar;
+ var closing = false;
+ var newdate = false;
+ var date = null;
+ if (typeof el.navtype == "undefined") {
+ if (cal.currentDateEl) {
+ Calendar.removeClass(cal.currentDateEl, "selected");
+ Calendar.addClass(el, "selected");
+ closing = (cal.currentDateEl == el);
+ if (!closing) {
+ cal.currentDateEl = el;
+ }
+ }
+ cal.date.setDateOnly(el.caldate);
+ date = cal.date;
+ var other_month = !(cal.dateClicked = !el.otherMonth);
+ if (!other_month && !cal.currentDateEl)
+ cal._toggleMultipleDate(new Date(date));
+ else
+ newdate = !el.disabled;
+ // a date was clicked
+ if (other_month)
+ cal._init(cal.firstDayOfWeek, date);
+ } else {
+ if (el.navtype == 200) {
+ Calendar.removeClass(el, "hilite");
+ cal.callCloseHandler();
+ return;
+ }
+ date = new Date(cal.date);
+ if (el.navtype == 0)
+ date.setDateOnly(new Date()); // TODAY
+ // unless "today" was clicked, we assume no date was clicked so
+ // the selected handler will know not to close the calenar when
+ // in single-click mode.
+ // cal.dateClicked = (el.navtype == 0);
+ cal.dateClicked = false;
+ var year = date.getFullYear();
+ var mon = date.getMonth();
+ function setMonth(m) {
+ var day = date.getDate();
+ var max = date.getMonthDays(m);
+ if (day > max) {
+ date.setDate(max);
+ }
+ date.setMonth(m);
+ };
+ switch (el.navtype) {
+ case 400:
+ Calendar.removeClass(el, "hilite");
+ var text = Calendar._TT["ABOUT"];
+ if (typeof text != "undefined") {
+ text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : "";
+ } else {
+ // FIXME: this should be removed as soon as lang files get updated!
+ text = "Help and about box text is not translated into this language.\n" +
+ "If you know this language and you feel generous please update\n" +
+ "the corresponding file in \"lang\" subdir to match calendar-en.js\n" +
+ "and send it back to <mihai_bazon@yahoo.com> to get it into the distribution ;-)\n\n" +
+ "Thank you!\n" +
+ "http://dynarch.com/mishoo/calendar.epl\n";
+ }
+ alert(text);
+ return;
+ case -2:
+ if (year > cal.minYear) {
+ date.setFullYear(year - 1);
+ }
+ break;
+ case -1:
+ if (mon > 0) {
+ setMonth(mon - 1);
+ } else if (year-- > cal.minYear) {
+ date.setFullYear(year);
+ setMonth(11);
+ }
+ break;
+ case 1:
+ if (mon < 11) {
+ setMonth(mon + 1);
+ } else if (year < cal.maxYear) {
+ date.setFullYear(year + 1);
+ setMonth(0);
+ }
+ break;
+ case 2:
+ if (year < cal.maxYear) {
+ date.setFullYear(year + 1);
+ }
+ break;
+ case 100:
+ cal.setFirstDayOfWeek(el.fdow);
+ return;
+ case 50:
+ var range = el._range;
+ var current = el.innerHTML;
+ for (var i = range.length; --i >= 0;)
+ if (range[i] == current)
+ break;
+ if (ev && ev.shiftKey) {
+ if (--i < 0)
+ i = range.length - 1;
+ } else if ( ++i >= range.length )
+ i = 0;
+ var newval = range[i];
+ el.innerHTML = newval;
+ cal.onUpdateTime();
+ return;
+ case 0:
+ // TODAY will bring us here
+ if ((typeof cal.getDateStatus == "function") &&
+ cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) {
+ return false;
+ }
+ break;
+ }
+ if (!date.equalsTo(cal.date)) {
+ cal.setDate(date);
+ newdate = true;
+ } else if (el.navtype == 0)
+ newdate = closing = true;
+ }
+ if (newdate) {
+ ev && cal.callHandler();
+ }
+ if (closing) {
+ Calendar.removeClass(el, "hilite");
+ ev && cal.callCloseHandler();
+ }
+};
+
+// END: CALENDAR STATIC FUNCTIONS
+
+// BEGIN: CALENDAR OBJECT FUNCTIONS
+
+/**
+ * This function creates the calendar inside the given parent. If _par is
+ * null than it creates a popup calendar inside the BODY element. If _par is
+ * an element, be it BODY, then it creates a non-popup calendar (still
+ * hidden). Some properties need to be set before calling this function.
+ */
+Calendar.prototype.create = function (_par) {
+ var parent = null;
+ if (! _par) {
+ // default parent is the document body, in which case we create
+ // a popup calendar.
+ parent = document.getElementsByTagName("body")[0];
+ this.isPopup = true;
+ } else {
+ parent = _par;
+ this.isPopup = false;
+ }
+ this.date = this.dateStr ? new Date(this.dateStr) : new Date();
+
+ var table = Calendar.createElement("table");
+ this.table = table;
+ table.cellSpacing = 0;
+ table.cellPadding = 0;
+ table.calendar = this;
+ Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown);
+
+ var div = Calendar.createElement("div");
+ this.element = div;
+ div.className = "calendar";
+ if (this.isPopup) {
+ div.style.position = "absolute";
+ div.style.display = "none";
+ }
+ div.appendChild(table);
+
+ var thead = Calendar.createElement("thead", table);
+ var cell = null;
+ var row = null;
+
+ var cal = this;
+ var hh = function (text, cs, navtype) {
+ cell = Calendar.createElement("td", row);
+ cell.colSpan = cs;
+ cell.className = "button";
+ if (navtype != 0 && Math.abs(navtype) <= 2)
+ cell.className += " nav";
+ Calendar._add_evs(cell);
+ cell.calendar = cal;
+ cell.navtype = navtype;
+ cell.innerHTML = "<div unselectable='on'>" + text + "</div>";
+ return cell;
+ };
+
+ row = Calendar.createElement("tr", thead);
+ var title_length = 6;
+ (this.isPopup) && --title_length;
+ (this.weekNumbers) && ++title_length;
+
+ hh("?", 1, 400).ttip = Calendar._TT["INFO"];
+ this.title = hh("", title_length, 300);
+ this.title.className = "title";
+ if (this.isPopup) {
+ this.title.ttip = Calendar._TT["DRAG_TO_MOVE"];
+ this.title.style.cursor = "move";
+ hh("×", 1, 200).ttip = Calendar._TT["CLOSE"];
+ }
+
+ row = Calendar.createElement("tr", thead);
+ row.className = "headrow";
+
+ this._nav_py = hh("«", 1, -2);
+ this._nav_py.ttip = Calendar._TT["PREV_YEAR"];
+
+ this._nav_pm = hh("‹", 1, -1);
+ this._nav_pm.ttip = Calendar._TT["PREV_MONTH"];
+
+ this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0);
+ this._nav_now.ttip = Calendar._TT["GO_TODAY"];
+
+ this._nav_nm = hh("›", 1, 1);
+ this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"];
+
+ this._nav_ny = hh("»", 1, 2);
+ this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"];
+
+ // day names
+ row = Calendar.createElement("tr", thead);
+ row.className = "daynames";
+ if (this.weekNumbers) {
+ cell = Calendar.createElement("td", row);
+ cell.className = "name wn";
+ cell.innerHTML = Calendar._TT["WK"];
+ }
+ for (var i = 7; i > 0; --i) {
+ cell = Calendar.createElement("td", row);
+ if (!i) {
+ cell.navtype = 100;
+ cell.calendar = this;
+ Calendar._add_evs(cell);
+ }
+ }
+ this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild;
+ this._displayWeekdays();
+
+ var tbody = Calendar.createElement("tbody", table);
+ this.tbody = tbody;
+
+ for (i = 6; i > 0; --i) {
+ row = Calendar.createElement("tr", tbody);
+ if (this.weekNumbers) {
+ cell = Calendar.createElement("td", row);
+ }
+ for (var j = 7; j > 0; --j) {
+ cell = Calendar.createElement("td", row);
+ cell.calendar = this;
+ Calendar._add_evs(cell);
+ }
+ }
+
+ if (this.showsTime) {
+ row = Calendar.createElement("tr", tbody);
+ row.className = "time";
+
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = 2;
+ cell.innerHTML = Calendar._TT["TIME"] || " ";
+
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = this.weekNumbers ? 4 : 3;
+
+ (function(){
+ function makeTimePart(className, init, range_start, range_end) {
+ var part = Calendar.createElement("span", cell);
+ part.className = className;
+ part.innerHTML = init;
+ part.calendar = cal;
+ part.ttip = Calendar._TT["TIME_PART"];
+ part.navtype = 50;
+ part._range = [];
+ if (typeof range_start != "number")
+ part._range = range_start;
+ else {
+ for (var i = range_start; i <= range_end; ++i) {
+ var txt;
+ if (i < 10 && range_end >= 10) txt = '0' + i;
+ else txt = '' + i;
+ part._range[part._range.length] = txt;
+ }
+ }
+ Calendar._add_evs(part);
+ return part;
+ };
+ var hrs = cal.date.getHours();
+ var mins = cal.date.getMinutes();
+ var t12 = !cal.time24;
+ var pm = (hrs > 12);
+ if (t12 && pm) hrs -= 12;
+ var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23);
+ var span = Calendar.createElement("span", cell);
+ span.innerHTML = ":";
+ span.className = "colon";
+ var M = makeTimePart("minute", mins, 0, 59);
+ var AP = null;
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = 2;
+ if (t12)
+ AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]);
+ else
+ cell.innerHTML = " ";
+
+ cal.onSetTime = function() {
+ var pm, hrs = this.date.getHours(),
+ mins = this.date.getMinutes();
+ if (t12) {
+ pm = (hrs >= 12);
+ if (pm) hrs -= 12;
+ if (hrs == 0) hrs = 12;
+ AP.innerHTML = pm ? "pm" : "am";
+ }
+ H.innerHTML = (hrs < 10) ? ("0" + hrs) : hrs;
+ M.innerHTML = (mins < 10) ? ("0" + mins) : mins;
+ };
+
+ cal.onUpdateTime = function() {
+ var date = this.date;
+ var h = parseInt(H.innerHTML, 10);
+ if (t12) {
+ if (/pm/i.test(AP.innerHTML) && h < 12)
+ h += 12;
+ else if (/am/i.test(AP.innerHTML) && h == 12)
+ h = 0;
+ }
+ var d = date.getDate();
+ var m = date.getMonth();
+ var y = date.getFullYear();
+ date.setHours(h);
+ date.setMinutes(parseInt(M.innerHTML, 10));
+ date.setFullYear(y);
+ date.setMonth(m);
+ date.setDate(d);
+ this.dateClicked = false;
+ this.callHandler();
+ };
+ })();
+ } else {
+ this.onSetTime = this.onUpdateTime = function() {};
+ }
+
+ var tfoot = Calendar.createElement("tfoot", table);
+
+ row = Calendar.createElement("tr", tfoot);
+ row.className = "footrow";
+
+ cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300);
+ cell.className = "ttip";
+ if (this.isPopup) {
+ cell.ttip = Calendar._TT["DRAG_TO_MOVE"];
+ cell.style.cursor = "move";
+ }
+ this.tooltips = cell;
+
+ div = Calendar.createElement("div", this.element);
+ this.monthsCombo = div;
+ div.className = "combo";
+ for (i = 0; i < Calendar._MN.length; ++i) {
+ var mn = Calendar.createElement("div");
+ mn.className = Calendar.is_ie ? "label-IEfix" : "label";
+ mn.month = i;
+ mn.innerHTML = Calendar._SMN[i];
+ div.appendChild(mn);
+ }
+
+ div = Calendar.createElement("div", this.element);
+ this.yearsCombo = div;
+ div.className = "combo";
+ for (i = 12; i > 0; --i) {
+ var yr = Calendar.createElement("div");
+ yr.className = Calendar.is_ie ? "label-IEfix" : "label";
+ div.appendChild(yr);
+ }
+
+ this._init(this.firstDayOfWeek, this.date);
+ parent.appendChild(this.element);
+};
+
+/** keyboard navigation, only for popup calendars */
+Calendar._keyEvent = function(ev) {
+ var cal = window._dynarch_popupCalendar;
+ if (!cal || cal.multiple)
+ return false;
+ (Calendar.is_ie) && (ev = window.event);
+ var act = (Calendar.is_ie || ev.type == "keypress"),
+ K = ev.keyCode;
+ if (ev.ctrlKey) {
+ switch (K) {
+ case 37: // KEY left
+ act && Calendar.cellClick(cal._nav_pm);
+ break;
+ case 38: // KEY up
+ act && Calendar.cellClick(cal._nav_py);
+ break;
+ case 39: // KEY right
+ act && Calendar.cellClick(cal._nav_nm);
+ break;
+ case 40: // KEY down
+ act && Calendar.cellClick(cal._nav_ny);
+ break;
+ default:
+ return false;
+ }
+ } else switch (K) {
+ case 32: // KEY space (now)
+ Calendar.cellClick(cal._nav_now);
+ break;
+ case 27: // KEY esc
+ act && cal.callCloseHandler();
+ break;
+ case 37: // KEY left
+ case 38: // KEY up
+ case 39: // KEY right
+ case 40: // KEY down
+ if (act) {
+ var prev, x, y, ne, el, step;
+ prev = K == 37 || K == 38;
+ step = (K == 37 || K == 39) ? 1 : 7;
+ function setVars() {
+ el = cal.currentDateEl;
+ var p = el.pos;
+ x = p & 15;
+ y = p >> 4;
+ ne = cal.ar_days[y][x];
+ };setVars();
+ function prevMonth() {
+ var date = new Date(cal.date);
+ date.setDate(date.getDate() - step);
+ cal.setDate(date);
+ };
+ function nextMonth() {
+ var date = new Date(cal.date);
+ date.setDate(date.getDate() + step);
+ cal.setDate(date);
+ };
+ while (1) {
+ switch (K) {
+ case 37: // KEY left
+ if (--x >= 0)
+ ne = cal.ar_days[y][x];
+ else {
+ x = 6;
+ K = 38;
+ continue;
+ }
+ break;
+ case 38: // KEY up
+ if (--y >= 0)
+ ne = cal.ar_days[y][x];
+ else {
+ prevMonth();
+ setVars();
+ }
+ break;
+ case 39: // KEY right
+ if (++x < 7)
+ ne = cal.ar_days[y][x];
+ else {
+ x = 0;
+ K = 40;
+ continue;
+ }
+ break;
+ case 40: // KEY down
+ if (++y < cal.ar_days.length)
+ ne = cal.ar_days[y][x];
+ else {
+ nextMonth();
+ setVars();
+ }
+ break;
+ }
+ break;
+ }
+ if (ne) {
+ if (!ne.disabled)
+ Calendar.cellClick(ne);
+ else if (prev)
+ prevMonth();
+ else
+ nextMonth();
+ }
+ }
+ break;
+ case 13: // KEY enter
+ if (act)
+ Calendar.cellClick(cal.currentDateEl, ev);
+ break;
+ default:
+ return false;
+ }
+ return Calendar.stopEvent(ev);
+};
+
+/**
+ * (RE)Initializes the calendar to the given date and firstDayOfWeek
+ */
+Calendar.prototype._init = function (firstDayOfWeek, date) {
+ var today = new Date(),
+ TY = today.getFullYear(),
+ TM = today.getMonth(),
+ TD = today.getDate();
+ this.table.style.visibility = "hidden";
+ var year = date.getFullYear();
+ if (year < this.minYear) {
+ year = this.minYear;
+ date.setFullYear(year);
+ } else if (year > this.maxYear) {
+ year = this.maxYear;
+ date.setFullYear(year);
+ }
+ this.firstDayOfWeek = firstDayOfWeek;
+ this.date = new Date(date);
+ var month = date.getMonth();
+ var mday = date.getDate();
+ var no_days = date.getMonthDays();
+
+ // calendar voodoo for computing the first day that would actually be
+ // displayed in the calendar, even if it's from the previous month.
+ // WARNING: this is magic. ;-)
+ date.setDate(1);
+ var day1 = (date.getDay() - this.firstDayOfWeek) % 7;
+ if (day1 < 0)
+ day1 += 7;
+ date.setDate(-day1);
+ date.setDate(date.getDate() + 1);
+
+ var row = this.tbody.firstChild;
+ var MN = Calendar._SMN[month];
+ var ar_days = this.ar_days = new Array();
+ var weekend = Calendar._TT["WEEKEND"];
+ var dates = this.multiple ? (this.datesCells = {}) : null;
+ for (var i = 0; i < 6; ++i, row = row.nextSibling) {
+ var cell = row.firstChild;
+ if (this.weekNumbers) {
+ cell.className = "day wn";
+ cell.innerHTML = date.getWeekNumber();
+ cell = cell.nextSibling;
+ }
+ row.className = "daysrow";
+ var hasdays = false, iday, dpos = ar_days[i] = [];
+ for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(iday + 1)) {
+ iday = date.getDate();
+ var wday = date.getDay();
+ cell.className = "day";
+ cell.pos = i << 4 | j;
+ dpos[j] = cell;
+ var current_month = (date.getMonth() == month);
+ if (!current_month) {
+ if (this.showsOtherMonths) {
+ cell.className += " othermonth";
+ cell.otherMonth = true;
+ } else {
+ cell.className = "emptycell";
+ cell.innerHTML = " ";
+ cell.disabled = true;
+ continue;
+ }
+ } else {
+ cell.otherMonth = false;
+ hasdays = true;
+ }
+ cell.disabled = false;
+ cell.innerHTML = this.getDateText ? this.getDateText(date, iday) : iday;
+ if (dates)
+ dates[date.print("%Y%m%d")] = cell;
+ if (this.getDateStatus) {
+ var status = this.getDateStatus(date, year, month, iday);
+ if (this.getDateToolTip) {
+ var toolTip = this.getDateToolTip(date, year, month, iday);
+ if (toolTip)
+ cell.title = toolTip;
+ }
+ if (status === true) {
+ cell.className += " disabled";
+ cell.disabled = true;
+ } else {
+ if (/disabled/i.test(status))
+ cell.disabled = true;
+ cell.className += " " + status;
+ }
+ }
+ if (!cell.disabled) {
+ cell.caldate = new Date(date);
+ cell.ttip = "_";
+ if (!this.multiple && current_month
+ && iday == mday && this.hiliteToday) {
+ cell.className += " selected";
+ this.currentDateEl = cell;
+ }
+ if (date.getFullYear() == TY &&
+ date.getMonth() == TM &&
+ iday == TD) {
+ cell.className += " today";
+ cell.ttip += Calendar._TT["PART_TODAY"];
+ }
+ if (weekend.indexOf(wday.toString()) != -1)
+ cell.className += cell.otherMonth ? " oweekend" : " weekend";
+ }
+ }
+ if (!(hasdays || this.showsOtherMonths))
+ row.className = "emptyrow";
+ }
+ this.title.innerHTML = Calendar._MN[month] + ", " + year;
+ this.onSetTime();
+ this.table.style.visibility = "visible";
+ this._initMultipleDates();
+ // PROFILE
+ // this.tooltips.innerHTML = "Generated in " + ((new Date()) - today) + " ms";
+};
+
+Calendar.prototype._initMultipleDates = function() {
+ if (this.multiple) {
+ for (var i in this.multiple) {
+ var cell = this.datesCells[i];
+ var d = this.multiple[i];
+ if (!d)
+ continue;
+ if (cell)
+ cell.className += " selected";
+ }
+ }
+};
+
+Calendar.prototype._toggleMultipleDate = function(date) {
+ if (this.multiple) {
+ var ds = date.print("%Y%m%d");
+ var cell = this.datesCells[ds];
+ if (cell) {
+ var d = this.multiple[ds];
+ if (!d) {
+ Calendar.addClass(cell, "selected");
+ this.multiple[ds] = date;
+ } else {
+ Calendar.removeClass(cell, "selected");
+ delete this.multiple[ds];
+ }
+ }
+ }
+};
+
+Calendar.prototype.setDateToolTipHandler = function (unaryFunction) {
+ this.getDateToolTip = unaryFunction;
+};
+
+/**
+ * Calls _init function above for going to a certain date (but only if the
+ * date is different than the currently selected one).
+ */
+Calendar.prototype.setDate = function (date) {
+ if (!date.equalsTo(this.date)) {
+ this._init(this.firstDayOfWeek, date);
+ }
+};
+
+/**
+ * Refreshes the calendar. Useful if the "disabledHandler" function is
+ * dynamic, meaning that the list of disabled date can change at runtime.
+ * Just * call this function if you think that the list of disabled dates
+ * should * change.
+ */
+Calendar.prototype.refresh = function () {
+ this._init(this.firstDayOfWeek, this.date);
+};
+
+/** Modifies the "firstDayOfWeek" parameter (pass 0 for Synday, 1 for Monday, etc.). */
+Calendar.prototype.setFirstDayOfWeek = function (firstDayOfWeek) {
+ this._init(firstDayOfWeek, this.date);
+ this._displayWeekdays();
+};
+
+/**
+ * Allows customization of what dates are enabled. The "unaryFunction"
+ * parameter must be a function object that receives the date (as a JS Date
+ * object) and returns a boolean value. If the returned value is true then
+ * the passed date will be marked as disabled.
+ */
+Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) {
+ this.getDateStatus = unaryFunction;
+};
+
+/** Customization of allowed year range for the calendar. */
+Calendar.prototype.setRange = function (a, z) {
+ this.minYear = a;
+ this.maxYear = z;
+};
+
+/** Calls the first user handler (selectedHandler). */
+Calendar.prototype.callHandler = function () {
+ if (this.onSelected) {
+ this.onSelected(this, this.date.print(this.dateFormat));
+ }
+};
+
+/** Calls the second user handler (closeHandler). */
+Calendar.prototype.callCloseHandler = function () {
+ if (this.onClose) {
+ this.onClose(this);
+ }
+ this.hideShowCovered();
+};
+
+/** Removes the calendar object from the DOM tree and destroys it. */
+Calendar.prototype.destroy = function () {
+ var el = this.element.parentNode;
+ el.removeChild(this.element);
+ Calendar._C = null;
+ window._dynarch_popupCalendar = null;
+};
+
+/**
+ * Moves the calendar element to a different section in the DOM tree (changes
+ * its parent).
+ */
+Calendar.prototype.reparent = function (new_parent) {
+ var el = this.element;
+ el.parentNode.removeChild(el);
+ new_parent.appendChild(el);
+};
+
+// This gets called when the user presses a mouse button anywhere in the
+// document, if the calendar is shown. If the click was outside the open
+// calendar this function closes it.
+Calendar._checkCalendar = function(ev) {
+ var calendar = window._dynarch_popupCalendar;
+ if (!calendar) {
+ return false;
+ }
+ var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
+ for (; el != null && el != calendar.element; el = el.parentNode);
+ if (el == null) {
+ // calls closeHandler which should hide the calendar.
+ window._dynarch_popupCalendar.callCloseHandler();
+ return Calendar.stopEvent(ev);
+ }
+};
+
+/** Shows the calendar. */
+Calendar.prototype.show = function () {
+ var rows = this.table.getElementsByTagName("tr");
+ for (var i = rows.length; i > 0;) {
+ var row = rows[--i];
+ Calendar.removeClass(row, "rowhilite");
+ var cells = row.getElementsByTagName("td");
+ for (var j = cells.length; j > 0;) {
+ var cell = cells[--j];
+ Calendar.removeClass(cell, "hilite");
+ Calendar.removeClass(cell, "active");
+ }
+ }
+ this.element.style.display = "block";
+ this.hidden = false;
+ if (this.isPopup) {
+ window._dynarch_popupCalendar = this;
+ Calendar.addEvent(document, "keydown", Calendar._keyEvent);
+ Calendar.addEvent(document, "keypress", Calendar._keyEvent);
+ Calendar.addEvent(document, "mousedown", Calendar._checkCalendar);
+ }
+ this.hideShowCovered();
+};
+
+/**
+ * Hides the calendar. Also removes any "hilite" from the class of any TD
+ * element.
+ */
+Calendar.prototype.hide = function () {
+ if (this.isPopup) {
+ Calendar.removeEvent(document, "keydown", Calendar._keyEvent);
+ Calendar.removeEvent(document, "keypress", Calendar._keyEvent);
+ Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar);
+ }
+ this.element.style.display = "none";
+ this.hidden = true;
+ this.hideShowCovered();
+};
+
+/**
+ * Shows the calendar at a given absolute position (beware that, depending on
+ * the calendar element style -- position property -- this might be relative
+ * to the parent's containing rectangle).
+ */
+Calendar.prototype.showAt = function (x, y) {
+ var s = this.element.style;
+ s.left = x + "px";
+ s.top = y + "px";
+ this.show();
+};
+
+/** Shows the calendar near a given element. */
+Calendar.prototype.showAtElement = function (el, opts) {
+ var self = this;
+ var p = Calendar.getAbsolutePos(el);
+ if (!opts || typeof opts != "string") {
+ this.showAt(p.x, p.y + el.offsetHeight);
+ return true;
+ }
+ function fixPosition(box) {
+ if (box.x < 0)
+ box.x = 0;
+ if (box.y < 0)
+ box.y = 0;
+ var cp = document.createElement("div");
+ var s = cp.style;
+ s.position = "absolute";
+ s.right = s.bottom = s.width = s.height = "0px";
+ document.body.appendChild(cp);
+ var br = Calendar.getAbsolutePos(cp);
+ document.body.removeChild(cp);
+ if (Calendar.is_ie) {
+ br.y += document.body.scrollTop;
+ br.x += document.body.scrollLeft;
+ } else {
+ br.y += window.scrollY;
+ br.x += window.scrollX;
+ }
+ var tmp = box.x + box.width - br.x;
+ if (tmp > 0) box.x -= tmp;
+ tmp = box.y + box.height - br.y;
+ if (tmp > 0) box.y -= tmp;
+ };
+ this.element.style.display = "block";
+ Calendar.continuation_for_the_fucking_khtml_browser = function() {
+ var w = self.element.offsetWidth;
+ var h = self.element.offsetHeight;
+ self.element.style.display = "none";
+ var valign = opts.substr(0, 1);
+ var halign = "l";
+ if (opts.length > 1) {
+ halign = opts.substr(1, 1);
+ }
+ // vertical alignment
+ switch (valign) {
+ case "T": p.y -= h; break;
+ case "B": p.y += el.offsetHeight; break;
+ case "C": p.y += (el.offsetHeight - h) / 2; break;
+ case "t": p.y += el.offsetHeight - h; break;
+ case "b": break; // already there
+ }
+ // horizontal alignment
+ switch (halign) {
+ case "L": p.x -= w; break;
+ case "R": p.x += el.offsetWidth; break;
+ case "C": p.x += (el.offsetWidth - w) / 2; break;
+ case "l": p.x += el.offsetWidth - w; break;
+ case "r": break; // already there
+ }
+ p.width = w;
+ p.height = h + 40;
+ self.monthsCombo.style.display = "none";
+ fixPosition(p);
+ self.showAt(p.x, p.y);
+ };
+ if (Calendar.is_khtml)
+ setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10);
+ else
+ Calendar.continuation_for_the_fucking_khtml_browser();
+};
+
+/** Customizes the date format. */
+Calendar.prototype.setDateFormat = function (str) {
+ this.dateFormat = str;
+};
+
+/** Customizes the tooltip date format. */
+Calendar.prototype.setTtDateFormat = function (str) {
+ this.ttDateFormat = str;
+};
+
+/**
+ * Tries to identify the date represented in a string. If successful it also
+ * calls this.setDate which moves the calendar to the given date.
+ */
+Calendar.prototype.parseDate = function(str, fmt) {
+ if (!fmt)
+ fmt = this.dateFormat;
+ this.setDate(Date.parseDate(str, fmt));
+};
+
+Calendar.prototype.hideShowCovered = function () {
+ if (!Calendar.is_ie && !Calendar.is_opera)
+ return;
+ function getVisib(obj){
+ var value = obj.style.visibility;
+ if (!value) {
+ if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C
+ if (!Calendar.is_khtml)
+ value = document.defaultView.
+ getComputedStyle(obj, "").getPropertyValue("visibility");
+ else
+ value = '';
+ } else if (obj.currentStyle) { // IE
+ value = obj.currentStyle.visibility;
+ } else
+ value = '';
+ }
+ return value;
+ };
+
+ var tags = new Array("applet", "iframe", "select");
+ var el = this.element;
+
+ var p = Calendar.getAbsolutePos(el);
+ var EX1 = p.x;
+ var EX2 = el.offsetWidth + EX1;
+ var EY1 = p.y;
+ var EY2 = el.offsetHeight + EY1;
+
+ for (var k = tags.length; k > 0; ) {
+ var ar = document.getElementsByTagName(tags[--k]);
+ var cc = null;
+
+ for (var i = ar.length; i > 0;) {
+ cc = ar[--i];
+
+ p = Calendar.getAbsolutePos(cc);
+ var CX1 = p.x;
+ var CX2 = cc.offsetWidth + CX1;
+ var CY1 = p.y;
+ var CY2 = cc.offsetHeight + CY1;
+
+ if (this.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) {
+ if (!cc.__msh_save_visibility) {
+ cc.__msh_save_visibility = getVisib(cc);
+ }
+ cc.style.visibility = cc.__msh_save_visibility;
+ } else {
+ if (!cc.__msh_save_visibility) {
+ cc.__msh_save_visibility = getVisib(cc);
+ }
+ cc.style.visibility = "hidden";
+ }
+ }
+ }
+};
+
+/** Internal function; it displays the bar with the names of the weekday. */
+Calendar.prototype._displayWeekdays = function () {
+ var fdow = this.firstDayOfWeek;
+ var cell = this.firstdayname;
+ var weekend = Calendar._TT["WEEKEND"];
+ for (var i = 0; i < 7; ++i) {
+ cell.className = "day name";
+ var realday = (i + fdow) % 7;
+ if (i) {
+ cell.ttip = Calendar._TT["DAY_FIRST"].replace("%s", Calendar._DN[realday]);
+ cell.navtype = 100;
+ cell.calendar = this;
+ cell.fdow = realday;
+ Calendar._add_evs(cell);
+ }
+ if (weekend.indexOf(realday.toString()) != -1) {
+ Calendar.addClass(cell, "weekend");
+ }
+ cell.innerHTML = Calendar._SDN[(i + fdow) % 7];
+ cell = cell.nextSibling;
+ }
+};
+
+/** Internal function. Hides all combo boxes that might be displayed. */
+Calendar.prototype._hideCombos = function () {
+ this.monthsCombo.style.display = "none";
+ this.yearsCombo.style.display = "none";
+};
+
+/** Internal function. Starts dragging the element. */
+Calendar.prototype._dragStart = function (ev) {
+ if (this.dragging) {
+ return;
+ }
+ this.dragging = true;
+ var posX;
+ var posY;
+ if (Calendar.is_ie) {
+ posY = window.event.clientY + document.body.scrollTop;
+ posX = window.event.clientX + document.body.scrollLeft;
+ } else {
+ posY = ev.clientY + window.scrollY;
+ posX = ev.clientX + window.scrollX;
+ }
+ var st = this.element.style;
+ this.xOffs = posX - parseInt(st.left);
+ this.yOffs = posY - parseInt(st.top);
+ with (Calendar) {
+ addEvent(document, "mousemove", calDragIt);
+ addEvent(document, "mouseup", calDragEnd);
+ }
+};
+
+// BEGIN: DATE OBJECT PATCHES
+
+/** Adds the number of days array to the Date object. */
+Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
+
+/** Constants used for time computations */
+Date.SECOND = 1000 /* milliseconds */;
+Date.MINUTE = 60 * Date.SECOND;
+Date.HOUR = 60 * Date.MINUTE;
+Date.DAY = 24 * Date.HOUR;
+Date.WEEK = 7 * Date.DAY;
+
+Date.parseDate = function(str, fmt) {
+ var today = new Date();
+ var y = 0;
+ var m = -1;
+ var d = 0;
+ var a = str.split(/\W+/);
+ var b = fmt.match(/%./g);
+ var i = 0, j = 0;
+ var hr = 0;
+ var min = 0;
+ for (i = 0; i < a.length; ++i) {
+ if (!a[i])
+ continue;
+ switch (b[i]) {
+ case "%d":
+ case "%e":
+ d = parseInt(a[i], 10);
+ break;
+
+ case "%m":
+ m = parseInt(a[i], 10) - 1;
+ break;
+
+ case "%Y":
+ case "%y":
+ y = parseInt(a[i], 10);
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
+ break;
+
+ case "%b":
+ case "%B":
+ for (j = 0; j < 12; ++j) {
+ if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; }
+ }
+ break;
+
+ case "%H":
+ case "%I":
+ case "%k":
+ case "%l":
+ hr = parseInt(a[i], 10);
+ break;
+
+ case "%P":
+ case "%p":
+ if (/pm/i.test(a[i]) && hr < 12)
+ hr += 12;
+ else if (/am/i.test(a[i]) && hr >= 12)
+ hr -= 12;
+ break;
+
+ case "%M":
+ min = parseInt(a[i], 10);
+ break;
+ }
+ }
+ if (isNaN(y)) y = today.getFullYear();
+ if (isNaN(m)) m = today.getMonth();
+ if (isNaN(d)) d = today.getDate();
+ if (isNaN(hr)) hr = today.getHours();
+ if (isNaN(min)) min = today.getMinutes();
+ if (y != 0 && m != -1 && d != 0)
+ return new Date(y, m, d, hr, min, 0);
+ y = 0; m = -1; d = 0;
+ for (i = 0; i < a.length; ++i) {
+ if (a[i].search(/[a-zA-Z]+/) != -1) {
+ var t = -1;
+ for (j = 0; j < 12; ++j) {
+ if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; }
+ }
+ if (t != -1) {
+ if (m != -1) {
+ d = m+1;
+ }
+ m = t;
+ }
+ } else if (parseInt(a[i], 10) <= 12 && m == -1) {
+ m = a[i]-1;
+ } else if (parseInt(a[i], 10) > 31 && y == 0) {
+ y = parseInt(a[i], 10);
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
+ } else if (d == 0) {
+ d = a[i];
+ }
+ }
+ if (y == 0)
+ y = today.getFullYear();
+ if (m != -1 && d != 0)
+ return new Date(y, m, d, hr, min, 0);
+ return today;
+};
+
+/** Returns the number of days in the current month */
+Date.prototype.getMonthDays = function(month) {
+ var year = this.getFullYear();
+ if (typeof month == "undefined") {
+ month = this.getMonth();
+ }
+ if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) {
+ return 29;
+ } else {
+ return Date._MD[month];
+ }
+};
+
+/** Returns the number of day in the year. */
+Date.prototype.getDayOfYear = function() {
+ var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+ var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0);
+ var time = now - then;
+ return Math.floor(time / Date.DAY);
+};
+
+/** Returns the number of the week in year, as defined in ISO 8601. */
+Date.prototype.getWeekNumber = function() {
+ var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+ var DoW = d.getDay();
+ d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu
+ var ms = d.valueOf(); // GMT
+ d.setMonth(0);
+ d.setDate(4); // Thu in Week 1
+ return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1;
+};
+
+/** Checks date and time equality */
+Date.prototype.equalsTo = function(date) {
+ return ((this.getFullYear() == date.getFullYear()) &&
+ (this.getMonth() == date.getMonth()) &&
+ (this.getDate() == date.getDate()) &&
+ (this.getHours() == date.getHours()) &&
+ (this.getMinutes() == date.getMinutes()));
+};
+
+/** Set only the year, month, date parts (keep existing time) */
+Date.prototype.setDateOnly = function(date) {
+ var tmp = new Date(date);
+ this.setDate(1);
+ this.setFullYear(tmp.getFullYear());
+ this.setMonth(tmp.getMonth());
+ this.setDate(tmp.getDate());
+};
+
+/** Prints the date in a string according to the given format. */
+Date.prototype.print = function (str) {
+ var m = this.getMonth();
+ var d = this.getDate();
+ var y = this.getFullYear();
+ var wn = this.getWeekNumber();
+ var w = this.getDay();
+ var s = {};
+ var hr = this.getHours();
+ var pm = (hr >= 12);
+ var ir = (pm) ? (hr - 12) : hr;
+ var dy = this.getDayOfYear();
+ if (ir == 0)
+ ir = 12;
+ var min = this.getMinutes();
+ var sec = this.getSeconds();
+ s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N]
+ s["%A"] = Calendar._DN[w]; // full weekday name
+ s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N]
+ s["%B"] = Calendar._MN[m]; // full month name
+ // FIXME: %c : preferred date and time representation for the current locale
+ s["%C"] = 1 + Math.floor(y / 100); // the century number
+ s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31)
+ s["%e"] = d; // the day of the month (range 1 to 31)
+ // FIXME: %D : american date style: %m/%d/%y
+ // FIXME: %E, %F, %G, %g, %h (man strftime)
+ s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format)
+ s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format)
+ s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366)
+ s["%k"] = hr; // hour, range 0 to 23 (24h format)
+ s["%l"] = ir; // hour, range 1 to 12 (12h format)
+ s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12
+ s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59
+ s["%n"] = "\n"; // a newline character
+ s["%p"] = pm ? "PM" : "AM";
+ s["%P"] = pm ? "pm" : "am";
+ // FIXME: %r : the time in am/pm notation %I:%M:%S %p
+ // FIXME: %R : the time in 24-hour notation %H:%M
+ s["%s"] = Math.floor(this.getTime() / 1000);
+ s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59
+ s["%t"] = "\t"; // a tab character
+ // FIXME: %T : the time in 24-hour notation (%H:%M:%S)
+ s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn;
+ s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON)
+ s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN)
+ // FIXME: %x : preferred date representation for the current locale without the time
+ // FIXME: %X : preferred time representation for the current locale without the date
+ s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99)
+ s["%Y"] = y; // year with the century
+ s["%%"] = "%"; // a literal '%' character
+
+ var re = /%./g;
+ if (!Calendar.is_ie5 && !Calendar.is_khtml)
+ return str.replace(re, function (par) { return s[par] || par; });
+
+ var a = str.match(re);
+ for (var i = 0; i < a.length; i++) {
+ var tmp = s[a[i]];
+ if (tmp) {
+ re = new RegExp(a[i], 'g');
+ str = str.replace(re, tmp);
+ }
+ }
+
+ return str;
+};
+
+Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear;
+Date.prototype.setFullYear = function(y) {
+ var d = new Date(this);
+ d.__msh_oldSetFullYear(y);
+ if (d.getMonth() != this.getMonth())
+ this.setDate(28);
+ this.__msh_oldSetFullYear(y);
+};
+
+// END: DATE OBJECT PATCHES
+
+
+// global object that remembers the calendar
+window._dynarch_popupCalendar = null;
--- /dev/null
+// ** I18N
+
+// Calendar BG language
+// Author: Nikolay Solakov, <thoranga@gmail.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Неделя",
+ "Понеделник",
+ "Вторник",
+ "Сряда",
+ "Четвъртък",
+ "Петък",
+ "Събота",
+ "Неделя");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Нед",
+ "Пон",
+ "Вто",
+ "Сря",
+ "Чет",
+ "Пет",
+ "Съб",
+ "Нед");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Януари",
+ "Февруари",
+ "Март",
+ "Април",
+ "Май",
+ "Юни",
+ "Юли",
+ "Август",
+ "Септември",
+ "Октомври",
+ "Ноември",
+ "Декември");
+
+// short month names
+Calendar._SMN = new Array
+("Яну",
+ "Фев",
+ "Мар",
+ "Апр",
+ "Май",
+ "Юни",
+ "Юли",
+ "Авг",
+ "Сеп",
+ "Окт",
+ "Ное",
+ "Дек");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "За календара";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Избор на дата:\n" +
+"- Използвайте \xab, \xbb за избор на година\n" +
+"- Използвайте " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " за избор на месец\n" +
+"- Задръжте натиснат бутона за списък с години/месеци.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Избор на час:\n" +
+"- Кликнете на числата от часа за да ги увеличите\n" +
+"- или Shift-click за намаляването им\n" +
+"- или кликнете и влачете за по-бърза промяна.";
+
+Calendar._TT["PREV_YEAR"] = "Предишна година (задръжте за списък)";
+Calendar._TT["PREV_MONTH"] = "Предишен месец (задръжте за списък)";
+Calendar._TT["GO_TODAY"] = "Днешна дата";
+Calendar._TT["NEXT_MONTH"] = "Следващ месец (задръжте за списък)";
+Calendar._TT["NEXT_YEAR"] = "Следваща година (задръжте за списък)";
+Calendar._TT["SEL_DATE"] = "Избор на дата";
+Calendar._TT["DRAG_TO_MOVE"] = "Дръпнете за преместване";
+Calendar._TT["PART_TODAY"] = " (днес)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Седмицата започва с %s";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Затвори";
+Calendar._TT["TODAY"] = "Днес";
+Calendar._TT["TIME_PART"] = "(Shift-)Click или влачене за промяна на стойност";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "седм";
+Calendar._TT["TIME"] = "Час:";
--- /dev/null
+// ** I18N
+
+// Calendar BS language
+// Autor: Ernad Husremović <hernad@bring.out.ba>
+//
+// Preuzeto od Dragan Matic, <kkid@panforma.co.yu>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Nedjelja",
+ "Ponedeljak",
+ "Utorak",
+ "Srijeda",
+ "Četvrtak",
+ "Petak",
+ "Subota",
+ "Nedelja");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Ned",
+ "Pon",
+ "Uto",
+ "Sri",
+ "Čet",
+ "Pet",
+ "Sub",
+ "Ned");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("Januar",
+ "Februar",
+ "Mart",
+ "April",
+ "Maj",
+ "Jun",
+ "Jul",
+ "Avgust",
+ "Septembar",
+ "Oktobar",
+ "Novembar",
+ "Decembar");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Maj",
+ "Jun",
+ "Jul",
+ "Avg",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "O kalendaru";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Preth. godina (drži pritisnuto za meni)";
+Calendar._TT["PREV_MONTH"] = "Preth. mjesec (drži pritisnuto za meni)";
+Calendar._TT["GO_TODAY"] = "Na današnji dan";
+Calendar._TT["NEXT_MONTH"] = "Naredni mjesec (drži pritisnuto za meni)";
+Calendar._TT["NEXT_YEAR"] = "Naredna godina (drži prisnuto za meni)";
+Calendar._TT["SEL_DATE"] = "Izbor datuma";
+Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za izmjenu";
+Calendar._TT["PART_TODAY"] = " (danas)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Prikaži %s prvo";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Zatvori";
+Calendar._TT["TODAY"] = "Danas";
+Calendar._TT["TIME_PART"] = "(Shift-)Klik ili prevlačenje za izmjenu vrijednosti";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Vrijeme:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Diumenge",
+ "Dilluns",
+ "Dimarts",
+ "Dimecres",
+ "Dijous",
+ "Divendres",
+ "Dissabte",
+ "Diumenge");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("dg",
+ "dl",
+ "dt",
+ "dc",
+ "dj",
+ "dv",
+ "ds",
+ "dg");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("Gener",
+ "Febrer",
+ "Març",
+ "Abril",
+ "Maig",
+ "Juny",
+ "Juliol",
+ "Agost",
+ "Setembre",
+ "Octubre",
+ "Novembre",
+ "Desembre");
+
+// short month names
+Calendar._SMN = new Array
+("Gen",
+ "Feb",
+ "Mar",
+ "Abr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Set",
+ "Oct",
+ "Nov",
+ "Des");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Quant al calendari";
+
+Calendar._TT["ABOUT"] =
+"Selector DHTML de data/hora\n" +
+"(c) dynarch.com 2002-2005 / Autor: Mihai Bazon\n" + // don't translate this this ;-)
+"Per a aconseguir l'última versió visiteu: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuït sota la llicència GNU LGPL. Vegeu http://gnu.org/licenses/lgpl.html per a més detalls." +
+"\n\n" +
+"Selecció de la data:\n" +
+"- Utilitzeu els botons \xab, \xbb per a seleccionar l'any\n" +
+"- Utilitzeu els botons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " per a selecciona el mes\n" +
+"- Mantingueu premut el botó del ratolí sobre qualsevol d'aquests botons per a uns selecció més ràpida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selecció de l'hora:\n" +
+"- Feu clic en qualsevol part de l'hora per a incrementar-la\n" +
+"- o premeu majúscules per a disminuir-la\n" +
+"- o feu clic i arrossegueu per a una selecció més ràpida.";
+
+Calendar._TT["PREV_YEAR"] = "Any anterior (mantenir per menú)";
+Calendar._TT["PREV_MONTH"] = "Mes anterior (mantenir per menú)";
+Calendar._TT["GO_TODAY"] = "Anar a avui";
+Calendar._TT["NEXT_MONTH"] = "Mes següent (mantenir per menú)";
+Calendar._TT["NEXT_YEAR"] = "Any següent (mantenir per menú)";
+Calendar._TT["SEL_DATE"] = "Sel·lecciona data";
+Calendar._TT["DRAG_TO_MOVE"] = "Arrossega per a moure";
+Calendar._TT["PART_TODAY"] = " (avui)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Primer mostra el %s";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Tanca";
+Calendar._TT["TODAY"] = "Avui";
+Calendar._TT["TIME_PART"] = "(Majúscules-)Feu clic o arrossegueu per a canviar el valor";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%A, %e de %B de %Y";
+
+Calendar._TT["WK"] = "set";
+Calendar._TT["TIME"] = "Hora:";
--- /dev/null
+/*
+ calendar-cs-win.js
+ language: Czech
+ encoding: windows-1250
+ author: Lubos Jerabek (xnet@seznam.cz)
+ Jan Uhlir (espinosa@centrum.cz)
+*/
+
+// ** I18N
+Calendar._DN = new Array('Neděle','Pondělí','Úterý','Středa','Čtvrtek','Pátek','Sobota','Neděle');
+Calendar._SDN = new Array('Ne','Po','Út','St','Čt','Pá','So','Ne');
+Calendar._MN = new Array('Leden','Únor','Březen','Duben','Květen','Červen','Červenec','Srpen','Září','Říjen','Listopad','Prosinec');
+Calendar._SMN = new Array('Led','Úno','Bře','Dub','Kvě','Črv','Čvc','Srp','Zář','Říj','Lis','Pro');
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "O komponentě kalendář";
+Calendar._TT["TOGGLE"] = "Změna prvního dne v týdnu";
+Calendar._TT["PREV_YEAR"] = "Předchozí rok (přidrž pro menu)";
+Calendar._TT["PREV_MONTH"] = "Předchozí měsíc (přidrž pro menu)";
+Calendar._TT["GO_TODAY"] = "Dnešní datum";
+Calendar._TT["NEXT_MONTH"] = "Další měsíc (přidrž pro menu)";
+Calendar._TT["NEXT_YEAR"] = "Další rok (přidrž pro menu)";
+Calendar._TT["SEL_DATE"] = "Vyber datum";
+Calendar._TT["DRAG_TO_MOVE"] = "Chyť a táhni, pro přesun";
+Calendar._TT["PART_TODAY"] = " (dnes)";
+Calendar._TT["MON_FIRST"] = "Ukaž jako první Pondělí";
+//Calendar._TT["SUN_FIRST"] = "Ukaž jako první Neděli";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Výběr datumu:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Použijte tlačítka " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " k výběru měsíce\n" +
+"- Podržte tlačítko myši na jakémkoliv z těch tlačítek pro rychlejší výběr.";
+
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Výběr času:\n" +
+"- Klikněte na jakoukoliv z částí výběru času pro zvýšení.\n" +
+"- nebo Shift-click pro snížení\n" +
+"- nebo klikněte a táhněte pro rychlejší výběr.";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Zobraz %s první";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Zavřít";
+Calendar._TT["TODAY"] = "Dnes";
+Calendar._TT["TIME_PART"] = "(Shift-)Klikni nebo táhni pro změnu hodnoty";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "d.m.yy";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Čas:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Translater: Mads N. Vestergaard <mnv@coolsms.dk>
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Søndag",
+ "Mandag",
+ "Tirsdag",
+ "Onsdag",
+ "Torsdag",
+ "Fredag",
+ "Lørdag",
+ "Søndag");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Søn",
+ "Man",
+ "Tir",
+ "Ons",
+ "Tor",
+ "Fre",
+ "Lør",
+ "Søn");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Januar",
+ "Februar",
+ "Marts",
+ "April",
+ "Maj",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Maj",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Om denne kalender";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For seneste version, besøg: http://www.dynarch.com/projects/calendar/\n" +
+"Distribueret under GNU LGPL. Se http://gnu.org/licenses/lgpl.html for detaljer." +
+"\n\n" +
+"Dato valg:\n" +
+"- Benyt \xab, \xbb tasterne til at vælge år\n" +
+"- Benyt " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tasterne til at vælge måned\n" +
+"- Hold musetasten inde på punkterne for at vælge hurtigere.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Tids valg:\n" +
+"- Klik på en af tidsrammerne for at forhøje det\n" +
+"- eller Shift-klik for at mindske det\n" +
+"- eller klik og træk for hurtigere valg.";
+
+Calendar._TT["PREV_YEAR"] = "Forrige år (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Forrige måned (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Gå til dags dato";
+Calendar._TT["NEXT_MONTH"] = "Næste måned (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Næste år (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Vælg dato";
+Calendar._TT["DRAG_TO_MOVE"] = "Træk for at flytte";
+Calendar._TT["PART_TODAY"] = " (dags dato)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Vis %s først";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "6,7";
+
+Calendar._TT["CLOSE"] = "Luk";
+Calendar._TT["TODAY"] = "I dag";
+Calendar._TT["TIME_PART"] = "(Shift-)Klik eller træk for at ændre værdi";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "uge";
+Calendar._TT["TIME"] = "Tid:";
--- /dev/null
+// ** I18N
+
+// Calendar DE language
+// Author: Jack (tR), <jack@jtr.de>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sonntag",
+ "Montag",
+ "Dienstag",
+ "Mittwoch",
+ "Donnerstag",
+ "Freitag",
+ "Samstag",
+ "Sonntag");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// short day names
+Calendar._SDN = new Array
+("So",
+ "Mo",
+ "Di",
+ "Mi",
+ "Do",
+ "Fr",
+ "Sa",
+ "So");
+
+// full month names
+Calendar._MN = new Array
+("Januar",
+ "Februar",
+ "M\u00e4rz",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "M\u00e4r",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dez");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "\u00DCber dieses Kalendarmodul";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Datum ausw\u00e4hlen:\n" +
+"- Benutzen Sie die \xab, \xbb Buttons um das Jahr zu w\u00e4hlen\n" +
+"- Benutzen Sie die " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " Buttons um den Monat zu w\u00e4hlen\n" +
+"- F\u00fcr eine Schnellauswahl halten Sie die Maustaste \u00fcber diesen Buttons fest.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Zeit ausw\u00e4hlen:\n" +
+"- Klicken Sie auf die Teile der Uhrzeit, um diese zu erh\u00F6hen\n" +
+"- oder klicken Sie mit festgehaltener Shift-Taste um diese zu verringern\n" +
+"- oder klicken und festhalten f\u00fcr Schnellauswahl.";
+
+Calendar._TT["TOGGLE"] = "Ersten Tag der Woche w\u00e4hlen";
+Calendar._TT["PREV_YEAR"] = "Voriges Jahr (Festhalten f\u00fcr Schnellauswahl)";
+Calendar._TT["PREV_MONTH"] = "Voriger Monat (Festhalten f\u00fcr Schnellauswahl)";
+Calendar._TT["GO_TODAY"] = "Heute ausw\u00e4hlen";
+Calendar._TT["NEXT_MONTH"] = "N\u00e4chst. Monat (Festhalten f\u00fcr Schnellauswahl)";
+Calendar._TT["NEXT_YEAR"] = "N\u00e4chst. Jahr (Festhalten f\u00fcr Schnellauswahl)";
+Calendar._TT["SEL_DATE"] = "Datum ausw\u00e4hlen";
+Calendar._TT["DRAG_TO_MOVE"] = "Zum Bewegen festhalten";
+Calendar._TT["PART_TODAY"] = " (Heute)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Woche beginnt mit %s ";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Schlie\u00dfen";
+Calendar._TT["TODAY"] = "Heute";
+Calendar._TT["TIME_PART"] = "(Shift-)Klick oder Festhalten und Ziehen um den Wert zu \u00e4ndern";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Zeit:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "About the calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Go Today";
+Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Select date";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
+Calendar._TT["PART_TODAY"] = " (today)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Display %s first";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Close";
+Calendar._TT["TODAY"] = "Today";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Time:";
--- /dev/null
+// ** I18N
+
+// Calendar ES (spanish) language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Updater: Servilio Afre Puentes <servilios@yahoo.com>
+// Updated: 2004-06-03
+// Encoding: utf-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Domingo",
+ "Lunes",
+ "Martes",
+ "Miércoles",
+ "Jueves",
+ "Viernes",
+ "Sábado",
+ "Domingo");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dom",
+ "Lun",
+ "Mar",
+ "Mié",
+ "Jue",
+ "Vie",
+ "Sáb",
+ "Dom");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Enero",
+ "Febrero",
+ "Marzo",
+ "Abril",
+ "Mayo",
+ "Junio",
+ "Julio",
+ "Agosto",
+ "Septiembre",
+ "Octubre",
+ "Noviembre",
+ "Diciembre");
+
+// short month names
+Calendar._SMN = new Array
+("Ene",
+ "Feb",
+ "Mar",
+ "Abr",
+ "May",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dic");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Acerca del calendario";
+
+Calendar._TT["ABOUT"] =
+"Selector DHTML de Fecha/Hora\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"Para conseguir la última versión visite: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuido bajo licencia GNU LGPL. Visite http://gnu.org/licenses/lgpl.html para más detalles." +
+"\n\n" +
+"Selección de fecha:\n" +
+"- Use los botones \xab, \xbb para seleccionar el año\n" +
+"- Use los botones " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para seleccionar el mes\n" +
+"- Mantenga pulsado el ratón en cualquiera de estos botones para una selección rápida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selección de hora:\n" +
+"- Pulse en cualquiera de las partes de la hora para incrementarla\n" +
+"- o pulse las mayúsculas mientras hace clic para decrementarla\n" +
+"- o haga clic y arrastre el ratón para una selección más rápida.";
+
+Calendar._TT["PREV_YEAR"] = "Año anterior (mantener para menú)";
+Calendar._TT["PREV_MONTH"] = "Mes anterior (mantener para menú)";
+Calendar._TT["GO_TODAY"] = "Ir a hoy";
+Calendar._TT["NEXT_MONTH"] = "Mes siguiente (mantener para menú)";
+Calendar._TT["NEXT_YEAR"] = "Año siguiente (mantener para menú)";
+Calendar._TT["SEL_DATE"] = "Seleccionar fecha";
+Calendar._TT["DRAG_TO_MOVE"] = "Arrastrar para mover";
+Calendar._TT["PART_TODAY"] = " (hoy)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Hacer %s primer día de la semana";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Cerrar";
+Calendar._TT["TODAY"] = "Hoy";
+Calendar._TT["TIME_PART"] = "(Mayúscula-)Clic o arrastre para cambiar valor";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%A, %e de %B de %Y";
+
+Calendar._TT["WK"] = "sem";
+Calendar._TT["TIME"] = "Hora:";
--- /dev/null
+// ** I18N
+
+// Calendar FI language
+// Author: Antti Perkiömäki <antti.perkiomaki@gmail.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sunnuntai",
+ "Maanantai",
+ "Tiistai",
+ "Keskiviikko",
+ "Torstai",
+ "Perjantai",
+ "Lauantai",
+ "Sunnuntai");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Su",
+ "Ma",
+ "Ti",
+ "Ke",
+ "To",
+ "Pe",
+ "La",
+ "Su");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Tammikuu",
+ "Helmikuu",
+ "Maaliskuu",
+ "Huhtikuu",
+ "Toukokuu",
+ "Kesäkuu",
+ "Heinäkuu",
+ "Elokuu",
+ "Syyskuu",
+ "Lokakuu",
+ "Marraskuu",
+ "Joulukuu");
+
+// short month names
+Calendar._SMN = new Array
+("Tammi",
+ "Helmi",
+ "Maalis",
+ "Huhti",
+ "Touko",
+ "Kesä",
+ "Heinä",
+ "Elo",
+ "Syys",
+ "Loka",
+ "Marras",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Tietoa kalenterista";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Tekijä: Mihai Bazon\n" + // don't translate this this ;-)
+"Viimeisin versio: http://www.dynarch.com/projects/calendar/\n" +
+"Jaettu GNU LGPL alaisena. Katso lisätiedot http://gnu.org/licenses/lgpl.html" +
+"\n\n" +
+"Päivä valitsin:\n" +
+"- Käytä \xab, \xbb painikkeita valitaksesi vuoden\n" +
+"- Käytä " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " painikkeita valitaksesi kuukauden\n" +
+"- Pidä alhaalla hiiren painiketta missä tahansa yllämainituissa painikkeissa valitaksesi nopeammin.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Ajan valinta:\n" +
+"- Paina mitä tahansa ajan osaa kasvattaaksesi sitä\n" +
+"- tai Vaihtonäppäin-paina laskeaksesi sitä\n" +
+"- tai paina ja raahaa valitaksesi nopeammin.";
+
+Calendar._TT["PREV_YEAR"] = "Edellinen vuosi (valikko tulee painaessa)";
+Calendar._TT["PREV_MONTH"] = "Edellinen kuukausi (valikko tulee painaessa)";
+Calendar._TT["GO_TODAY"] = "Siirry Tänään";
+Calendar._TT["NEXT_MONTH"] = "Seuraava kuukausi (valikko tulee painaessa)";
+Calendar._TT["NEXT_YEAR"] = "Seuraava vuosi (valikko tulee painaessa)";
+Calendar._TT["SEL_DATE"] = "Valitse päivä";
+Calendar._TT["DRAG_TO_MOVE"] = "Rahaa siirtääksesi";
+Calendar._TT["PART_TODAY"] = " (tänään)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Näytä %s ensin";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "6,0";
+
+Calendar._TT["CLOSE"] = "Sulje";
+Calendar._TT["TODAY"] = "Tänään";
+Calendar._TT["TIME_PART"] = "(Vaihtonäppäin-)Paina tai raahaa vaihtaaksesi arvoa";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "vko";
+Calendar._TT["TIME"] = "Aika:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// Translator: David Duret, <pilgrim@mala-template.net> from previous french version
+
+// full day names
+Calendar._DN = new Array
+("Dimanche",
+ "Lundi",
+ "Mardi",
+ "Mercredi",
+ "Jeudi",
+ "Vendredi",
+ "Samedi",
+ "Dimanche");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dim",
+ "Lun",
+ "Mar",
+ "Mer",
+ "Jeu",
+ "Ven",
+ "Sam",
+ "Dim");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Janvier",
+ "Février",
+ "Mars",
+ "Avril",
+ "Mai",
+ "Juin",
+ "Juillet",
+ "Août",
+ "Septembre",
+ "Octobre",
+ "Novembre",
+ "Décembre");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Fev",
+ "Mar",
+ "Avr",
+ "Mai",
+ "Juin",
+ "Juil",
+ "Aout",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "A propos du calendrier";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Heure Selecteur\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"Pour la derniere version visitez : http://www.dynarch.com/projects/calendar/\n" +
+"Distribué par GNU LGPL. Voir http://gnu.org/licenses/lgpl.html pour les details." +
+"\n\n" +
+"Selection de la date :\n" +
+"- Utiliser les bouttons \xab, \xbb pour selectionner l\'annee\n" +
+"- Utiliser les bouttons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pour selectionner les mois\n" +
+"- Garder la souris sur n'importe quels boutons pour une selection plus rapide";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selection de l\'heure :\n" +
+"- Cliquer sur heures ou minutes pour incrementer\n" +
+"- ou Maj-clic pour decrementer\n" +
+"- ou clic et glisser-deplacer pour une selection plus rapide";
+
+Calendar._TT["PREV_YEAR"] = "Année préc. (maintenir pour menu)";
+Calendar._TT["PREV_MONTH"] = "Mois préc. (maintenir pour menu)";
+Calendar._TT["GO_TODAY"] = "Atteindre la date du jour";
+Calendar._TT["NEXT_MONTH"] = "Mois suiv. (maintenir pour menu)";
+Calendar._TT["NEXT_YEAR"] = "Année suiv. (maintenir pour menu)";
+Calendar._TT["SEL_DATE"] = "Sélectionner une date";
+Calendar._TT["DRAG_TO_MOVE"] = "Déplacer";
+Calendar._TT["PART_TODAY"] = " (Aujourd'hui)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Afficher %s en premier";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Fermer";
+Calendar._TT["TODAY"] = "Aujourd'hui";
+Calendar._TT["TIME_PART"] = "(Maj-)Clic ou glisser pour modifier la valeur";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "Sem.";
+Calendar._TT["TIME"] = "Heure :";
--- /dev/null
+// ** I18N
+
+// Calendar GL (galician) language
+// Author: Martín Vázquez Cabanas, <eu@martinvazquez.net>
+// Updated: 2009-01-23
+// Encoding: utf-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Domingo",
+ "Luns",
+ "Martes",
+ "Mércores",
+ "Xoves",
+ "Venres",
+ "Sábado",
+ "Domingo");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dom",
+ "Lun",
+ "Mar",
+ "Mér",
+ "Xov",
+ "Ven",
+ "Sáb",
+ "Dom");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Xaneiro",
+ "Febreiro",
+ "Marzo",
+ "Abril",
+ "Maio",
+ "Xuño",
+ "Xullo",
+ "Agosto",
+ "Setembro",
+ "Outubro",
+ "Novembro",
+ "Decembro");
+
+// short month names
+Calendar._SMN = new Array
+("Xan",
+ "Feb",
+ "Mar",
+ "Abr",
+ "Mai",
+ "Xun",
+ "Xull",
+ "Ago",
+ "Set",
+ "Out",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Acerca do calendario";
+
+Calendar._TT["ABOUT"] =
+"Selector DHTML de Data/Hora\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"Para conseguila última versión visite: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuído baixo licenza GNU LGPL. Visite http://gnu.org/licenses/lgpl.html para máis detalles." +
+"\n\n" +
+"Selección de data:\n" +
+"- Use os botóns \xab, \xbb para seleccionalo ano\n" +
+"- Use os botóns " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para seleccionalo mes\n" +
+"- Manteña pulsado o rato en calquera destes botóns para unha selección rápida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selección de hora:\n" +
+"- Pulse en calquera das partes da hora para incrementala\n" +
+"- ou pulse maiúsculas mentres fai clic para decrementala\n" +
+"- ou faga clic e arrastre o rato para unha selección máis rápida.";
+
+Calendar._TT["PREV_YEAR"] = "Ano anterior (manter para menú)";
+Calendar._TT["PREV_MONTH"] = "Mes anterior (manter para menú)";
+Calendar._TT["GO_TODAY"] = "Ir a hoxe";
+Calendar._TT["NEXT_MONTH"] = "Mes seguinte (manter para menú)";
+Calendar._TT["NEXT_YEAR"] = "Ano seguinte (manter para menú)";
+Calendar._TT["SEL_DATE"] = "Seleccionar data";
+Calendar._TT["DRAG_TO_MOVE"] = "Arrastrar para mover";
+Calendar._TT["PART_TODAY"] = " (hoxe)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Facer %s primeiro día da semana";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Pechar";
+Calendar._TT["TODAY"] = "Hoxe";
+Calendar._TT["TIME_PART"] = "(Maiúscula-)Clic ou arrastre para cambiar valor";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%A, %e de %B de %Y";
+
+Calendar._TT["WK"] = "sem";
+Calendar._TT["TIME"] = "Hora:";
--- /dev/null
+// ** I18N
+
+// Calendar HE language
+// Author: Saggi Mizrahi
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("ראשון",
+ "שני",
+ "שלישי",
+ "רביעי",
+ "חמישי",
+ "שישי",
+ "שבת",
+ "ראשון");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("א",
+ "ב",
+ "ג",
+ "ד",
+ "ה",
+ "ו",
+ "ש",
+ "א");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("ינואר",
+ "פברואר",
+ "מרץ",
+ "אפריל",
+ "מאי",
+ "יוני",
+ "יולי",
+ "אוגוסט",
+ "ספטמבר",
+ "אוקטובר",
+ "נובמבר",
+ "דצמבר");
+
+// short month names
+Calendar._SMN = new Array
+("ינו'",
+ "פבו'",
+ "מרץ",
+ "אפר'",
+ "מאי",
+ "יונ'",
+ "יול'",
+ "אוג'",
+ "ספט'",
+ "אוקט'",
+ "נוב'",
+ "דצמ'");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "אודות לוח השנה";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "שנה קודמת (החזק לתפריט)";
+Calendar._TT["PREV_MONTH"] = "חודש קודם (החזק לתפריט)";
+Calendar._TT["GO_TODAY"] = "לך להיום";
+Calendar._TT["NEXT_MONTH"] = "חודש הבא (החזק לתפריט)";
+Calendar._TT["NEXT_YEAR"] = "שנה הבאה (החזק לתפריט)";
+Calendar._TT["SEL_DATE"] = "בחר תאריך";
+Calendar._TT["DRAG_TO_MOVE"] = "משוך כדי להזיז";
+Calendar._TT["PART_TODAY"] = " (היום)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "הצג %s קודם";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "5,6";
+
+Calendar._TT["CLOSE"] = "סגור";
+Calendar._TT["TODAY"] = "היום";
+Calendar._TT["TIME_PART"] = "(Shift-)לחץ או משוך כדי לשנות את הערך";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "זמן:";
--- /dev/null
+// ** I18N
+
+// Calendar HU language
+// Author: Takács Gábor
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Vasárnap",
+ "Hétfő",
+ "Kedd",
+ "Szerda",
+ "Csütörtök",
+ "Péntek",
+ "Szombat",
+ "Vasárnap");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Vas",
+ "Hét",
+ "Ked",
+ "Sze",
+ "Csü",
+ "Pén",
+ "Szo",
+ "Vas");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Január",
+ "Február",
+ "Március",
+ "Április",
+ "Május",
+ "Június",
+ "Július",
+ "Augusztus",
+ "Szeptember",
+ "Október",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Már",
+ "Ápr",
+ "Máj",
+ "Jún",
+ "Júl",
+ "Aug",
+ "Szep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "A naptár leírása";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Előző év (nyomvatart = menü)";
+Calendar._TT["PREV_MONTH"] = "Előző hónap (nyomvatart = menü)";
+Calendar._TT["GO_TODAY"] = "Irány a Ma";
+Calendar._TT["NEXT_MONTH"] = "Következő hónap (nyomvatart = menü)";
+Calendar._TT["NEXT_YEAR"] = "Következő év (nyomvatart = menü)";
+Calendar._TT["SEL_DATE"] = "Válasszon dátumot";
+Calendar._TT["DRAG_TO_MOVE"] = "Fogd és vidd";
+Calendar._TT["PART_TODAY"] = " (ma)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "%s megjelenítése elsőként";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Bezár";
+Calendar._TT["TODAY"] = "Ma";
+Calendar._TT["TIME_PART"] = "(Shift-)Click vagy húzd az érték változtatásához";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y.%m.%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%B %e, %A";
+
+Calendar._TT["WK"] = "hét";
+Calendar._TT["TIME"] = "Idő:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Domenica",
+ "Lunedì",
+ "Martedì",
+ "Mercoledì",
+ "Giovedì",
+ "Venerdì",
+ "Sabato",
+ "Domenica");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dom",
+ "Lun",
+ "Mar",
+ "Mer",
+ "Gio",
+ "Ven",
+ "Sab",
+ "Dom");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Gennaio",
+ "Febbraio",
+ "Marzo",
+ "Aprile",
+ "Maggio",
+ "Giugno",
+ "Luglio",
+ "Agosto",
+ "Settembre",
+ "Ottobre",
+ "Novembre",
+ "Dicembre");
+
+// short month names
+Calendar._SMN = new Array
+("Gen",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Mag",
+ "Giu",
+ "Lug",
+ "Ago",
+ "Set",
+ "Ott",
+ "Nov",
+ "Dic");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Informazioni sul calendario";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Anno prec. (tieni premuto per menu)";
+Calendar._TT["PREV_MONTH"] = "Mese prec. (tieni premuto per menu)";
+Calendar._TT["GO_TODAY"] = "Oggi";
+Calendar._TT["NEXT_MONTH"] = "Mese succ. (tieni premuto per menu)";
+Calendar._TT["NEXT_YEAR"] = "Anno succ. (tieni premuto per menu)";
+Calendar._TT["SEL_DATE"] = "Seleziona data";
+Calendar._TT["DRAG_TO_MOVE"] = "Trascina per spostare";
+Calendar._TT["PART_TODAY"] = " (oggi)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Mostra %s per primo";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Chiudi";
+Calendar._TT["TODAY"] = "Oggi";
+Calendar._TT["TIME_PART"] = "(Shift-)Click o trascina per modificare";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "sett";
+Calendar._TT["TIME"] = "Ora:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array ("日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array ("日", "月", "火", "水", "木", "金", "土");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array ("1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月");
+
+// short month names
+Calendar._SMN = new Array ("1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "このカレンダーについて";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"日付の選択方法:\n" +
+"- \xab, \xbb ボタンで年を選択。\n" +
+"- " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " ボタンで年を選択。\n" +
+"- 上記ボタンの長押しでメニューから選択。";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "前年 (長押しでメニュー表示)";
+Calendar._TT["PREV_MONTH"] = "前月 (長押しでメニュー表示)";
+Calendar._TT["GO_TODAY"] = "今日の日付を選択";
+Calendar._TT["NEXT_MONTH"] = "翌月 (長押しでメニュー表示)";
+Calendar._TT["NEXT_YEAR"] = "翌年 (長押しでメニュー表示)";
+Calendar._TT["SEL_DATE"] = "日付を選択してください";
+Calendar._TT["DRAG_TO_MOVE"] = "ドラッグで移動";
+Calendar._TT["PART_TODAY"] = " (今日)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "%s始まりで表示";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "閉じる";
+Calendar._TT["TODAY"] = "今日";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%b%e日(%a)";
+
+Calendar._TT["WK"] = "週";
+Calendar._TT["TIME"] = "Time:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("일요일",
+ "월요일",
+ "화요일",
+ "수요일",
+ "목요일",
+ "금요일",
+ "토요일",
+ "일요일");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("일",
+ "월",
+ "화",
+ "수",
+ "목",
+ "금",
+ "토",
+ "일");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("1월",
+ "2월",
+ "3월",
+ "4월",
+ "5월",
+ "6월",
+ "7월",
+ "8월",
+ "9월",
+ "10월",
+ "11월",
+ "12월");
+
+// short month names
+Calendar._SMN = new Array
+("1월",
+ "2월",
+ "3월",
+ "4월",
+ "5월",
+ "6월",
+ "7월",
+ "8월",
+ "9월",
+ "10월",
+ "11월",
+ "12월");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "이 달력은 ... & 도움말";
+
+Calendar._TT["ABOUT"] =
+"DHTML 날짜/시간 선택기\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"최신 버전을 구하려면 여기로: http://www.dynarch.com/projects/calendar/\n" +
+"배포라이센스:GNU LGPL. 참조:http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"날짜 선택:\n" +
+"- 해를 선택하려면 \xab, \xbb 버튼을 사용하세요.\n" +
+"- 달을 선택하려면 " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " 버튼을 사용하세요.\n" +
+"- 좀 더 빠르게 선택하려면 위의 버튼을 꾹 눌러주세요.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"시간 선택:\n" +
+"- 시, 분을 더하려면 클릭하세요.\n" +
+"- 시, 분을 빼려면 쉬프트 누르고 클릭하세요.\n" +
+"- 좀 더 빠르게 선택하려면 클릭하고 드래그하세요.";
+
+Calendar._TT["PREV_YEAR"] = "이전 해";
+Calendar._TT["PREV_MONTH"] = "이전 달";
+Calendar._TT["GO_TODAY"] = "오늘로 이동";
+Calendar._TT["NEXT_MONTH"] = "다음 달";
+Calendar._TT["NEXT_YEAR"] = "다음 해";
+Calendar._TT["SEL_DATE"] = "날짜 선택";
+Calendar._TT["DRAG_TO_MOVE"] = "이동(드래그)";
+Calendar._TT["PART_TODAY"] = " (오늘)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "[%s]을 처음으로";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "닫기";
+Calendar._TT["TODAY"] = "오늘";
+Calendar._TT["TIME_PART"] = "클릭(+),쉬프트+클릭(-),드래그";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "주";
+Calendar._TT["TIME"] = "시간:";
--- /dev/null
+// ** I18N
+
+// Calendar LT language
+// Author: Gediminas Muižis, <gediminas.muizis@elgama.eu>
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+// Ver: 0.2
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sekmadienis",
+ "Pirmadienis",
+ "Antradienis",
+ "Trečiadienis",
+ "Ketvirtadienis",
+ "Penktadienis",
+ "Šeštadienis",
+ "Sekmadienis");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Sek",
+ "Pir",
+ "Ant",
+ "Tre",
+ "Ket",
+ "Pen",
+ "Šeš",
+ "Sek");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Sausis",
+ "Vasaris",
+ "Kovas",
+ "Balandis",
+ "Gegužė",
+ "Birželis",
+ "Liepa",
+ "Rudpjūtis",
+ "Rugsėjis",
+ "Spalis",
+ "Lapkritis",
+ "Gruodis");
+
+// short month names
+Calendar._SMN = new Array
+("Sau",
+ "Vas",
+ "Kov",
+ "Bal",
+ "Geg",
+ "Brž",
+ "Lie",
+ "Rgp",
+ "Rgs",
+ "Spl",
+ "Lap",
+ "Grd");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Apie kalendorių";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Datos pasirinkimas:\n" +
+"- Naudoti \xab, \xbb mygtukus norint pasirinkti metus\n" +
+"- Naudoti " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " mygtukus norint pasirinkti mėnesį\n" +
+"- PAlaikykite nuspaudę bet kurį nygtuką norėdami iškviesti greitąjį meniu.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Datos pasirinkimas:\n" +
+"- Paspaudus ant valandos ar minutės, jų reikšmės padidėja\n" +
+"- arba Shift-paspaudimas norint sumažinti reikšmę\n" +
+"- arba paspauskite ir tempkite norint greičiau keisti reikšmę.";
+
+Calendar._TT["PREV_YEAR"] = "Ankst. metai (laikyti, norint iškviesti meniu)";
+Calendar._TT["PREV_MONTH"] = "Ankst. mėnuo (laikyti, norint iškviesti meniu)";
+Calendar._TT["GO_TODAY"] = "Šiandien";
+Calendar._TT["NEXT_MONTH"] = "Kitas mėnuo (laikyti, norint iškviesti meniu)";
+Calendar._TT["NEXT_YEAR"] = "Kiti metai (laikyti, norint iškviesti meniu)";
+Calendar._TT["SEL_DATE"] = "Pasirinkti datą";
+Calendar._TT["DRAG_TO_MOVE"] = "Perkelkite pėlyte";
+Calendar._TT["PART_TODAY"] = " (šiandien)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Rodyti %s pirmiau";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Uždaryti";
+Calendar._TT["TODAY"] = "Šiandien";
+Calendar._TT["TIME_PART"] = "(Shift-)Spausti ar tempti, norint pakeisti reikšmę";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "sav";
+Calendar._TT["TIME"] = "Laikas:";
--- /dev/null
+// ** I18N
+
+// Calendar МК language
+// Author: Илин Татабитовски, <ilin@slobodensoftver.org.mk>
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("недела",
+ "понеделник",
+ "вторник",
+ "среда",
+ "четврток",
+ "петок",
+ "сабота",
+ "недела");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("нед",
+ "пон",
+ "вто",
+ "сре",
+ "чет",
+ "пет",
+ "саб",
+ "нед");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("јануари",
+ "февруари",
+ "март",
+ "април",
+ "мај",
+ "јуни",
+ "јули",
+ "август",
+ "септември",
+ "октомври",
+ "ноември",
+ "декември");
+
+// short month names
+Calendar._SMN = new Array
+("јан",
+ "фев",
+ "мар",
+ "апр",
+ "мај",
+ "јун",
+ "јул",
+ "авг",
+ "сеп",
+ "окт",
+ "ное",
+ "дек");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "За календарот";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Претходна година (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Претходен месец (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Go Today";
+Calendar._TT["NEXT_MONTH"] = "Следен месец (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Следна година (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Изберете дата";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
+Calendar._TT["PART_TODAY"] = " (денес)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Прикажи %s прво";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Затвори";
+Calendar._TT["TODAY"] = "Денес";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b";
+
+Calendar._TT["WK"] = "нед";
+Calendar._TT["TIME"] = "Време:";
--- /dev/null
+// ** I18N
+
+// Calendar NL language
+// Author: Linda van den Brink, <linda@dynasol.nl>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Zondag",
+ "Maandag",
+ "Dinsdag",
+ "Woensdag",
+ "Donderdag",
+ "Vrijdag",
+ "Zaterdag",
+ "Zondag");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Zo",
+ "Ma",
+ "Di",
+ "Wo",
+ "Do",
+ "Vr",
+ "Za",
+ "Zo");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("Januari",
+ "Februari",
+ "Maart",
+ "April",
+ "Mei",
+ "Juni",
+ "Juli",
+ "Augustus",
+ "September",
+ "Oktober",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Maa",
+ "Apr",
+ "Mei",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Over de kalender";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Datum selectie:\n" +
+"- Gebruik de \xab, \xbb knoppen om het jaar te selecteren\n" +
+"- Gebruik de " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " knoppen om de maand te selecteren\n" +
+"- Houd de muisknop ingedrukt op een van de knoppen voor snellere selectie.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Tijd selectie:\n" +
+"- Klik op een deel van de tijd om het te verhogen\n" +
+"- of Shift-click om het te verlagen\n" +
+"- of klik en sleep voor snellere selectie.";
+
+Calendar._TT["PREV_YEAR"] = "Vorig jaar (vasthouden voor menu)";
+Calendar._TT["PREV_MONTH"] = "Vorige maand (vasthouden voor menu)";
+Calendar._TT["GO_TODAY"] = "Ga naar vandaag";
+Calendar._TT["NEXT_MONTH"] = "Volgende maand (vasthouden voor menu)";
+Calendar._TT["NEXT_YEAR"] = "Volgend jaar(vasthouden voor menu)";
+Calendar._TT["SEL_DATE"] = "Selecteer datum";
+Calendar._TT["DRAG_TO_MOVE"] = "Sleep om te verplaatsen";
+Calendar._TT["PART_TODAY"] = " (vandaag)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Toon %s eerst";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Sluiten";
+Calendar._TT["TODAY"] = "Vandaag";
+Calendar._TT["TIME_PART"] = "(Shift-)klik of sleep om waarde te wijzigen";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Tijd:";
--- /dev/null
+// ** I18N
+
+// Calendar NO language (Norwegian/Norsk bokmål)
+// Author: Kai Olav Fredriksen <k@i.fredriksen.net>
+
+// full day names
+Calendar._DN = new Array
+("Søndag",
+ "Mandag",
+ "Tirsdag",
+ "Onsdag",
+ "Torsdag",
+ "Fredag",
+ "Lørdag",
+ "Søndag");
+
+Calendar._SDN_len = 3; // short day name length
+Calendar._SMN_len = 3; // short month name length
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Januar",
+ "Februar",
+ "Mars",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Desember");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Om kalenderen";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Forrige år (hold for meny)";
+Calendar._TT["PREV_MONTH"] = "Forrige måned (hold for meny)";
+Calendar._TT["GO_TODAY"] = "Gå til idag";
+Calendar._TT["NEXT_MONTH"] = "Neste måned (hold for meny)";
+Calendar._TT["NEXT_YEAR"] = "Neste år (hold for meny)";
+Calendar._TT["SEL_DATE"] = "Velg dato";
+Calendar._TT["DRAG_TO_MOVE"] = "Dra for å flytte";
+Calendar._TT["PART_TODAY"] = " (idag)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Vis %s først";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Lukk";
+Calendar._TT["TODAY"] = "Idag";
+Calendar._TT["TIME_PART"] = "(Shift-)Klikk eller dra for å endre verdi";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%%d.%m.%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "uke";
+Calendar._TT["TIME"] = "Tid:";
--- /dev/null
+// ** I18N\r
+\r
+// Calendar EN language\r
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>\r
+// Encoding: any\r
+// Distributed under the same terms as the calendar itself.\r
+\r
+// For translators: please use UTF-8 if possible. We strongly believe that\r
+// Unicode is the answer to a real internationalized world. Also please\r
+// include your contact information in the header, as can be seen above.\r
+\r
+// full day names\r
+Calendar._DN = new Array\r
+("Niedziela",\r
+ "Poniedziałek",\r
+ "Wtorek",\r
+ "Środa",\r
+ "Czwartek",\r
+ "Piątek",\r
+ "Sobota",\r
+ "Niedziela");\r
+\r
+// Please note that the following array of short day names (and the same goes\r
+// for short month names, _SMN) isn't absolutely necessary. We give it here\r
+// for exemplification on how one can customize the short day names, but if\r
+// they are simply the first N letters of the full name you can simply say:\r
+//\r
+// Calendar._SDN_len = N; // short day name length\r
+// Calendar._SMN_len = N; // short month name length\r
+//\r
+// If N = 3 then this is not needed either since we assume a value of 3 if not\r
+// present, to be compatible with translation files that were written before\r
+// this feature.\r
+\r
+// short day names\r
+Calendar._SDN = new Array\r
+("Nie",\r
+ "Pon",\r
+ "Wto",\r
+ "Śro",\r
+ "Czw",\r
+ "Pią",\r
+ "Sob",\r
+ "Nie");\r
+\r
+// First day of the week. "0" means display Sunday first, "1" means display\r
+// Monday first, etc.\r
+Calendar._FD = 1;\r
+\r
+// full month names\r
+Calendar._MN = new Array\r
+("Styczeń",\r
+ "Luty",\r
+ "Marzec",\r
+ "Kwiecień",\r
+ "Maj",\r
+ "Czerwiec",\r
+ "Lipiec",\r
+ "Sierpień",\r
+ "Wrzesień",\r
+ "Październik",\r
+ "Listopad",\r
+ "Grudzień");\r
+\r
+// short month names\r
+Calendar._SMN = new Array\r
+("Sty",\r
+ "Lut",\r
+ "Mar",\r
+ "Kwi",\r
+ "Maj",\r
+ "Cze",\r
+ "Lip",\r
+ "Sie",\r
+ "Wrz",\r
+ "Paź",\r
+ "Lis",\r
+ "Gru");\r
+\r
+// tooltips\r
+Calendar._TT = {};\r
+Calendar._TT["INFO"] = "O kalendarzu";\r
+\r
+Calendar._TT["ABOUT"] =\r
+"DHTML Date/Time Selector\n" +\r
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)\r
+"Po ostatnią wersję odwiedź: http://www.dynarch.com/projects/calendar/\n" +\r
+"Rozpowszechniany pod licencją GNU LGPL. Zobacz: http://gnu.org/licenses/lgpl.html z celu zapoznania się ze szczegółami." +\r
+"\n\n" +\r
+"Wybór daty:\n" +\r
+"- Użyj \xab, \xbb przycisków by zaznaczyć rok\n" +\r
+"- Użyj " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " przycisków by zaznaczyć miesiąc\n" +\r
+"- Trzymaj wciśnięty przycisk myszy na każdym z powyższych przycisków by przyśpieszyć zaznaczanie.";\r
+Calendar._TT["ABOUT_TIME"] = "\n\n" +\r
+"Wybór czasu:\n" +\r
+"- Kliknij na każdym przedziale czasu aby go powiększyć\n" +\r
+"- lub kliknij z przyciskiem Shift by go zmniejszyć\n" +\r
+"- lub kliknij i przeciągnij dla szybszego zaznaczenia.";\r
+\r
+Calendar._TT["PREV_YEAR"] = "Poprz. rok (przytrzymaj dla menu)";\r
+Calendar._TT["PREV_MONTH"] = "Poprz. miesiąc (przytrzymaj dla menu)";\r
+Calendar._TT["GO_TODAY"] = "Idź do Dzisiaj";\r
+Calendar._TT["NEXT_MONTH"] = "Następny miesiąc(przytrzymaj dla menu)";\r
+Calendar._TT["NEXT_YEAR"] = "Następny rok (przytrzymaj dla menu)";\r
+Calendar._TT["SEL_DATE"] = "Zaznacz datę";\r
+Calendar._TT["DRAG_TO_MOVE"] = "Przeciągnij by przenieść";\r
+Calendar._TT["PART_TODAY"] = " (dzisiaj)";\r
+\r
+// the following is to inform that "%s" is to be the first day of week\r
+// %s will be replaced with the day name.\r
+Calendar._TT["DAY_FIRST"] = "Pokaż %s pierwszy";\r
+\r
+// This may be locale-dependent. It specifies the week-end days, as an array\r
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1\r
+// means Monday, etc.\r
+Calendar._TT["WEEKEND"] = "0,6";\r
+\r
+Calendar._TT["CLOSE"] = "Zamknij";\r
+Calendar._TT["TODAY"] = "Dzisiaj";\r
+Calendar._TT["TIME_PART"] = "(Shift-)Kliknij lub upuść by zmienić wartość";\r
+\r
+// date formats\r
+Calendar._TT["DEF_DATE_FORMAT"] = "%R-%m-%d";\r
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";\r
+\r
+Calendar._TT["WK"] = "wk";\r
+Calendar._TT["TIME"] = "Czas:";\r
--- /dev/null
+// ** I18N
+
+// Calendar pt_BR language
+// Author: Adalberto Machado, <betosm@terra.com.br>
+// Review: Alexandre da Silva, <simpsomboy@gmail.com>
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Domingo",
+ "Segunda",
+ "Terça",
+ "Quarta",
+ "Quinta",
+ "Sexta",
+ "Sabado",
+ "Domingo");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dom",
+ "Seg",
+ "Ter",
+ "Qua",
+ "Qui",
+ "Sex",
+ "Sab",
+ "Dom");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("Janeiro",
+ "Fevereiro",
+ "Março",
+ "Abril",
+ "Maio",
+ "Junho",
+ "Julho",
+ "Agosto",
+ "Setembro",
+ "Outubro",
+ "Novembro",
+ "Dezembro");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Fev",
+ "Mar",
+ "Abr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Set",
+ "Out",
+ "Nov",
+ "Dez");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Sobre o calendário";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"Última versão visite: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuído sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." +
+"\n\n" +
+"Seleção de data:\n" +
+"- Use os botões \xab, \xbb para selecionar o ano\n" +
+"- Use os botões " + String.fromCharCode(0x2039) + ", " +
+String.fromCharCode(0x203a) + " para selecionar o mês\n" +
+"- Segure o botão do mouse em qualquer um desses botões para seleção rápida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Seleção de hora:\n" +
+"- Clique em qualquer parte da hora para incrementar\n" +
+"- ou Shift-click para decrementar\n" +
+"- ou clique e segure para seleção rápida.";
+
+Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)";
+Calendar._TT["PREV_MONTH"] = "Ant. mês (segure para menu)";
+Calendar._TT["GO_TODAY"] = "Hoje";
+Calendar._TT["NEXT_MONTH"] = "Próx. mes (segure para menu)";
+Calendar._TT["NEXT_YEAR"] = "Próx. ano (segure para menu)";
+Calendar._TT["SEL_DATE"] = "Selecione a data";
+Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover";
+Calendar._TT["PART_TODAY"] = " (hoje)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Mostre %s primeiro";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Fechar";
+Calendar._TT["TODAY"] = "Hoje";
+Calendar._TT["TIME_PART"] = "(Shift-)Click ou arraste para mudar valor";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b";
+
+Calendar._TT["WK"] = "sm";
+Calendar._TT["TIME"] = "Hora:";
--- /dev/null
+// ** I18N
+
+// Calendar pt language
+// Author: Adalberto Machado, <betosm@terra.com.br>
+// Corrected by: Pedro Araújo <phcrva19@hotmail.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Domingo",
+ "Segunda",
+ "Terça",
+ "Quarta",
+ "Quinta",
+ "Sexta",
+ "Sábado",
+ "Domingo");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dom",
+ "Seg",
+ "Ter",
+ "Qua",
+ "Qui",
+ "Sex",
+ "Sáb",
+ "Dom");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Janeiro",
+ "Fevereiro",
+ "Março",
+ "Abril",
+ "Maio",
+ "Junho",
+ "Julho",
+ "Agosto",
+ "Setembro",
+ "Outubro",
+ "Novembro",
+ "Dezembro");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Fev",
+ "Mar",
+ "Abr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Set",
+ "Out",
+ "Nov",
+ "Dez");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Sobre o calendário";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"Última versão visite: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuído sobre a licença GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." +
+"\n\n" +
+"Selecção de data:\n" +
+"- Use os botões \xab, \xbb para seleccionar o ano\n" +
+"- Use os botões " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para seleccionar o mês\n" +
+"- Segure o botão do rato em qualquer um desses botões para selecção rápida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selecção de hora:\n" +
+"- Clique em qualquer parte da hora para incrementar\n" +
+"- ou Shift-click para decrementar\n" +
+"- ou clique e segure para selecção rápida.";
+
+Calendar._TT["PREV_YEAR"] = "Ano ant. (segure para menu)";
+Calendar._TT["PREV_MONTH"] = "Mês ant. (segure para menu)";
+Calendar._TT["GO_TODAY"] = "Hoje";
+Calendar._TT["NEXT_MONTH"] = "Prox. mês (segure para menu)";
+Calendar._TT["NEXT_YEAR"] = "Prox. ano (segure para menu)";
+Calendar._TT["SEL_DATE"] = "Seleccione a data";
+Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover";
+Calendar._TT["PART_TODAY"] = " (hoje)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Mostre %s primeiro";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Fechar";
+Calendar._TT["TODAY"] = "Hoje";
+Calendar._TT["TIME_PART"] = "(Shift-)Click ou arraste para mudar valor";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b";
+
+Calendar._TT["WK"] = "sm";
+Calendar._TT["TIME"] = "Hora:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Duminică",
+ "Luni",
+ "Marți",
+ "Miercuri",
+ "Joi",
+ "Vineri",
+ "Sâmbătă",
+ "Duminică");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Dum",
+ "Lun",
+ "Mar",
+ "Mie",
+ "Joi",
+ "Vin",
+ "Sâm",
+ "Dum");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Ianuarie",
+ "Februarie",
+ "Martie",
+ "Aprilie",
+ "Mai",
+ "Iunie",
+ "Iulie",
+ "August",
+ "Septembrie",
+ "Octombrie",
+ "Noiembrie",
+ "Decembrie");
+
+// short month names
+Calendar._SMN = new Array
+("Ian",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Mai",
+ "Iun",
+ "Iul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Noi",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Despre calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Selectare data:\n" +
+"- Folositi butoanele \xab, \xbb pentru a selecta anul\n" +
+"- Folositi butoanele " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pentru a selecta luna\n" +
+"- Lasati apasat butonul pentru o selectie mai rapida.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Selectare timp:\n" +
+"- Click pe campul de timp pentru a majora timpul\n" +
+"- sau Shift-Click pentru a micsora\n" +
+"- sau click si drag pentru manipulare rapida.";
+
+Calendar._TT["PREV_YEAR"] = "Anul precedent (apasati pentru meniu)";
+Calendar._TT["PREV_MONTH"] = "Luna precedenta (apasati pentru meniu)";
+Calendar._TT["GO_TODAY"] = "Astazi";
+Calendar._TT["NEXT_MONTH"] = "Luna viitoare (apasati pentru meniu)";
+Calendar._TT["NEXT_YEAR"] = "Anul viitor (apasati pentru meniu)";
+Calendar._TT["SEL_DATE"] = "Selecteaza data";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag pentru a muta";
+Calendar._TT["PART_TODAY"] = " (azi)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Vizualizează %s prima";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Închide";
+Calendar._TT["TODAY"] = "Azi";
+Calendar._TT["TIME_PART"] = "(Shift-)Click sau drag pentru a schimba valoarea";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%A-%l-%z";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "săpt";
+Calendar._TT["TIME"] = "Ora:";
--- /dev/null
+// ** I18N
+
+// Calendar RU language
+// Translation: Sly Golovanov, http://golovanov.net, <sly@golovanov.net>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("воскресенье",
+ "понедельник",
+ "вторник",
+ "среда",
+ "четверг",
+ "пятница",
+ "суббота",
+ "воскресенье");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("вск",
+ "пон",
+ "втр",
+ "срд",
+ "чет",
+ "пят",
+ "суб",
+ "вск");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("январь",
+ "февраль",
+ "март",
+ "апрель",
+ "май",
+ "июнь",
+ "июль",
+ "август",
+ "сентябрь",
+ "октябрь",
+ "ноябрь",
+ "декабрь");
+
+// short month names
+Calendar._SMN = new Array
+("янв",
+ "фев",
+ "мар",
+ "апр",
+ "май",
+ "июн",
+ "июл",
+ "авг",
+ "сен",
+ "окт",
+ "ноя",
+ "дек");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "О календаре...";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Как выбрать дату:\n" +
+"- При помощи кнопок \xab, \xbb можно выбрать год\n" +
+"- При помощи кнопок " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " можно выбрать месяц\n" +
+"- Подержите эти кнопки нажатыми, чтобы появилось меню быстрого выбора.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Как выбрать время:\n" +
+"- При клике на часах или минутах они увеличиваются\n" +
+"- при клике с нажатой клавишей Shift они уменьшаются\n" +
+"- если нажать и двигать мышкой влево/вправо, они будут меняться быстрее.";
+
+Calendar._TT["PREV_YEAR"] = "На год назад (удерживать для меню)";
+Calendar._TT["PREV_MONTH"] = "На месяц назад (удерживать для меню)";
+Calendar._TT["GO_TODAY"] = "Сегодня";
+Calendar._TT["NEXT_MONTH"] = "На месяц вперед (удерживать для меню)";
+Calendar._TT["NEXT_YEAR"] = "На год вперед (удерживать для меню)";
+Calendar._TT["SEL_DATE"] = "Выберите дату";
+Calendar._TT["DRAG_TO_MOVE"] = "Перетаскивайте мышкой";
+Calendar._TT["PART_TODAY"] = " (сегодня)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Первый день недели будет %s";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Закрыть";
+Calendar._TT["TODAY"] = "Сегодня";
+Calendar._TT["TIME_PART"] = "(Shift-)клик или нажать и двигать";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%e %b, %a";
+
+Calendar._TT["WK"] = "нед";
+Calendar._TT["TIME"] = "Время:";
--- /dev/null
+/*
+ calendar-sk.js
+ language: Slovak
+ encoding: UTF-8
+ author: Stanislav Pach (stano.pach@seznam.cz)
+*/
+
+// ** I18N
+Calendar._DN = new Array('Nedeľa','Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota','Nedeľa');
+Calendar._SDN = new Array('Ne','Po','Ut','St','Št','Pi','So','Ne');
+Calendar._MN = new Array('Január','Február','Marec','Apríl','Máj','Jún','Júl','August','September','Október','November','December');
+Calendar._SMN = new Array('Jan','Feb','Mar','Apr','Máj','Jún','Júl','Aug','Sep','Okt','Nov','Dec');
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "O komponente kalendár";
+Calendar._TT["TOGGLE"] = "Zmena prvého dňa v týždni";
+Calendar._TT["PREV_YEAR"] = "Predchádzajúci rok (pridrž pre menu)";
+Calendar._TT["PREV_MONTH"] = "Predchádzajúci mesiac (pridrž pre menu)";
+Calendar._TT["GO_TODAY"] = "Dnešný dátum";
+Calendar._TT["NEXT_MONTH"] = "Ďalší mesiac (pridrž pre menu)";
+Calendar._TT["NEXT_YEAR"] = "Ďalší rok (pridrž pre menu)";
+Calendar._TT["SEL_DATE"] = "Zvoľ dátum";
+Calendar._TT["DRAG_TO_MOVE"] = "Chyť a ťahaj pre presun";
+Calendar._TT["PART_TODAY"] = " (dnes)";
+Calendar._TT["MON_FIRST"] = "Ukáž ako prvný Pondelok";
+//Calendar._TT["SUN_FIRST"] = "Ukaž jako první Neděli";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Výber dátumu:\n" +
+"- Použijte tlačítka \xab, \xbb pre voľbu roku\n" +
+"- Použijte tlačítka " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pre výber mesiaca\n" +
+"- Podržte tlačítko myši na akomkoľvek z týchto tlačítok pre rýchlejší výber.";
+
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Výber času:\n" +
+"- Kliknite na akúkoľvek časť z výberu času pre zvýšenie.\n" +
+"- alebo Shift-klick pre zníženie\n" +
+"- alebo kliknite a ťahajte pre rýchlejší výber.";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Zobraz %s ako prvý";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Zavrieť";
+Calendar._TT["TODAY"] = "Dnes";
+Calendar._TT["TIME_PART"] = "(Shift-)Klikni alebo ťahaj pre zmenu hodnoty";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "d.m.yy";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "týž";
+Calendar._TT["TIME"] = "Čas:";
--- /dev/null
+// ** I18N
+
+// Calendar SL language
+// Author: Jernej Vidmar, <jernej.vidmar@vidmarboehm.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Nedelja",
+ "Ponedeljek",
+ "Torek",
+ "Sreda",
+ "Četrtek",
+ "Petek",
+ "Sobota",
+ "Nedelja");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Ned",
+ "Pon",
+ "Tor",
+ "Sre",
+ "Čet",
+ "Pet",
+ "Sob",
+ "Ned");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("Januar",
+ "Februar",
+ "Marec",
+ "April",
+ "Maj",
+ "Junij",
+ "Julij",
+ "Avgust",
+ "September",
+ "Oktober",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Maj",
+ "Jun",
+ "Jul",
+ "Avg",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "O koledarju";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Izbira datuma:\n" +
+"- Uporabite \xab, \xbb gumbe za izbiro leta\n" +
+"- Uporabite " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " gumbe za izbiro meseca\n" +
+"- Za hitrejšo izbiro držite miškin gumb nad enim od zgornjih gumbov.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Izbira časa:\n" +
+"- Kliknite na katerikoli del časa da ga povečate\n" +
+"- oziroma kliknite s Shiftom za znižanje\n" +
+"- ali kliknite in vlecite za hitrejšo izbiro.";
+
+Calendar._TT["PREV_YEAR"] = "Prejšnje leto (držite za meni)";
+Calendar._TT["PREV_MONTH"] = "Prejšnji mesec (držite za meni)";
+Calendar._TT["GO_TODAY"] = "Pojdi na danes";
+Calendar._TT["NEXT_MONTH"] = "Naslednji mesec (držite za meni)";
+Calendar._TT["NEXT_YEAR"] = "Naslednje leto (držite za meni)";
+Calendar._TT["SEL_DATE"] = "Izberite datum";
+Calendar._TT["DRAG_TO_MOVE"] = "Povlecite za premik";
+Calendar._TT["PART_TODAY"] = " (danes)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Najprej prikaži %s";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Zapri";
+Calendar._TT["TODAY"] = "Danes";
+Calendar._TT["TIME_PART"] = "(Shift-)klik ali povleči, da spremeniš vrednost";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Time:";
--- /dev/null
+// ** I18N\r
+\r
+// Calendar SR language\r
+// Author: Dragan Matic, <kkid@panforma.co.yu>\r
+// Encoding: any\r
+// Distributed under the same terms as the calendar itself.\r
+\r
+// For translators: please use UTF-8 if possible. We strongly believe that\r
+// Unicode is the answer to a real internationalized world. Also please\r
+// include your contact information in the header, as can be seen above.\r
+\r
+// full day names\r
+Calendar._DN = new Array\r
+("Nedelja",\r
+ "Ponedeljak",\r
+ "Utorak",\r
+ "Sreda",\r
+ "Četvrtak",\r
+ "Petak",\r
+ "Subota",\r
+ "Nedelja");\r
+\r
+// Please note that the following array of short day names (and the same goes\r
+// for short month names, _SMN) isn't absolutely necessary. We give it here\r
+// for exemplification on how one can customize the short day names, but if\r
+// they are simply the first N letters of the full name you can simply say:\r
+//\r
+// Calendar._SDN_len = N; // short day name length\r
+// Calendar._SMN_len = N; // short month name length\r
+//\r
+// If N = 3 then this is not needed either since we assume a value of 3 if not\r
+// present, to be compatible with translation files that were written before\r
+// this feature.\r
+\r
+// short day names\r
+Calendar._SDN = new Array\r
+("Ned",\r
+ "Pon",\r
+ "Uto",\r
+ "Sre",\r
+ "Čet",\r
+ "Pet",\r
+ "Sub",\r
+ "Ned");\r
+\r
+// First day of the week. "0" means display Sunday first, "1" means display\r
+// Monday first, etc.\r
+Calendar._FD = 0;\r
+\r
+// full month names\r
+Calendar._MN = new Array\r
+("Januar",\r
+ "Februar",\r
+ "Mart",\r
+ "April",\r
+ "Maj",\r
+ "Jun",\r
+ "Jul",\r
+ "Avgust",\r
+ "Septembar",\r
+ "Oktobar",\r
+ "Novembar",\r
+ "Decembar");\r
+\r
+// short month names\r
+Calendar._SMN = new Array\r
+("Jan",\r
+ "Feb",\r
+ "Mar",\r
+ "Apr",\r
+ "Maj",\r
+ "Jun",\r
+ "Jul",\r
+ "Avg",\r
+ "Sep",\r
+ "Okt",\r
+ "Nov",\r
+ "Dec");\r
+\r
+// tooltips\r
+Calendar._TT = {};\r
+Calendar._TT["INFO"] = "O kalendaru";\r
+\r
+Calendar._TT["ABOUT"] =\r
+"DHTML Date/Time Selector\n" +\r
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)\r
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +\r
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +\r
+"\n\n" +\r
+"Date selection:\n" +\r
+"- Use the \xab, \xbb buttons to select year\n" +\r
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +\r
+"- Hold mouse button on any of the above buttons for faster selection.";\r
+Calendar._TT["ABOUT_TIME"] = "\n\n" +\r
+"Time selection:\n" +\r
+"- Click on any of the time parts to increase it\n" +\r
+"- or Shift-click to decrease it\n" +\r
+"- or click and drag for faster selection.";\r
+\r
+Calendar._TT["PREV_YEAR"] = "Preth. godina (hold for menu)";\r
+Calendar._TT["PREV_MONTH"] = "Preth. mesec (hold for menu)";\r
+Calendar._TT["GO_TODAY"] = "Na današnji dan";\r
+Calendar._TT["NEXT_MONTH"] = "Naredni mesec (hold for menu)";\r
+Calendar._TT["NEXT_YEAR"] = "Naredna godina (hold for menu)";\r
+Calendar._TT["SEL_DATE"] = "Izbor datuma";\r
+Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za izmenu";\r
+Calendar._TT["PART_TODAY"] = " (danas)";\r
+\r
+// the following is to inform that "%s" is to be the first day of week\r
+// %s will be replaced with the day name.\r
+Calendar._TT["DAY_FIRST"] = "Prikazi %s prvo";\r
+\r
+// This may be locale-dependent. It specifies the week-end days, as an array\r
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1\r
+// means Monday, etc.\r
+Calendar._TT["WEEKEND"] = "0,6";\r
+\r
+Calendar._TT["CLOSE"] = "Close";\r
+Calendar._TT["TODAY"] = "Danas";\r
+Calendar._TT["TIME_PART"] = "(Shift-)Klik ili prevlačenje za izmenu vrednosti";\r
+\r
+// date formats\r
+Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y";\r
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";\r
+\r
+Calendar._TT["WK"] = "wk";\r
+Calendar._TT["TIME"] = "Vreme:";\r
--- /dev/null
+// ** I18N
+
+// full day names
+Calendar._DN = new Array
+("Söndag",
+ "Måndag",
+ "Tisdag",
+ "Onsdag",
+ "Torsdag",
+ "Fredag",
+ "Lördag",
+ "Söndag");
+
+Calendar._SDN_len = 3; // short day name length
+Calendar._SMN_len = 3; // short month name length
+
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Januari",
+ "Februari",
+ "Mars",
+ "April",
+ "Maj",
+ "Juni",
+ "Juli",
+ "Augusti",
+ "September",
+ "Oktober",
+ "November",
+ "December");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Om kalendern";
+
+Calendar._TT["ABOUT"] =
+"DHTML Datum/Tid-väljare\n" +
+"(c) dynarch.com 2002-2005 / Upphovsman: Mihai Bazon\n" + // don't translate this this ;-)
+"För senaste version besök: http://www.dynarch.com/projects/calendar/\n" +
+"Distribueras under GNU LGPL. Se http://gnu.org/licenses/lgpl.html för detaljer." +
+"\n\n" +
+"Välja datum:\n" +
+"- Använd \xab, \xbb knapparna för att välja år\n" +
+"- Använd " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " knapparna för att välja månad\n" +
+"- Håll nere musknappen på någon av ovanstående knappar för att se snabbval.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Välja tid:\n" +
+"- Klicka på något av tidsfälten för att öka\n" +
+"- eller Skift-klicka för att minska\n" +
+"- eller klicka och dra för att välja snabbare.";
+
+Calendar._TT["PREV_YEAR"] = "Föreg. år (håll nere för lista)";
+Calendar._TT["PREV_MONTH"] = "Föreg. månad (håll nere för lista)";
+Calendar._TT["GO_TODAY"] = "Gå till Idag";
+Calendar._TT["NEXT_MONTH"] = "Nästa månad (håll nere för lista)";
+Calendar._TT["NEXT_YEAR"] = "Nästa år (håll nere för lista)";
+Calendar._TT["SEL_DATE"] = "Välj datum";
+Calendar._TT["DRAG_TO_MOVE"] = "Dra för att flytta";
+Calendar._TT["PART_TODAY"] = " (idag)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Visa %s först";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Stäng";
+Calendar._TT["TODAY"] = "Idag";
+Calendar._TT["TIME_PART"] = "(Skift-)klicka eller dra för att ändra värde";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "v.";
+Calendar._TT["TIME"] = "Tid:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Gampol Thitinilnithi, <gampolt@gmail.com>
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("อาทิตย์",
+ "จันทร์",
+ "อังคาร",
+ "พุธ",
+ "พฤหัสบดี",
+ "ศุกร์",
+ "เสาร์",
+ "อาทิตย์");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("อา.",
+ "จ.",
+ "อ.",
+ "พ.",
+ "พฤ.",
+ "ศ.",
+ "ส.",
+ "อา.");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("มกราคม",
+ "กุมภาพันธ์",
+ "มีนาคม",
+ "เมษายน",
+ "พฤษภาคม",
+ "มิถุนายน",
+ "กรกฎาคม",
+ "สิงหาคม",
+ "กันยายน",
+ "ตุลาคม",
+ "พฤศจิกายน",
+ "ธันวาคม");
+
+// short month names
+Calendar._SMN = new Array
+("ม.ค.",
+ "ก.พ.",
+ "มี.ค.",
+ "เม.ย.",
+ "พ.ค.",
+ "มิ.ย.",
+ "ก.ค.",
+ "ส.ค.",
+ "ก.ย.",
+ "ต.ค.",
+ "พ.ย.",
+ "ธ.ค.");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "เกี่ยวกับปฏิทิน";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "ปีที่แล้ว (ถ้ากดค้างจะมีเมนู)";
+Calendar._TT["PREV_MONTH"] = "เดือนที่แล้ว (ถ้ากดค้างจะมีเมนู)";
+Calendar._TT["GO_TODAY"] = "ไปที่วันนี้";
+Calendar._TT["NEXT_MONTH"] = "เดือนหน้า (ถ้ากดค้างจะมีเมนู)";
+Calendar._TT["NEXT_YEAR"] = "ปีหน้า (ถ้ากดค้างจะมีเมนู)";
+Calendar._TT["SEL_DATE"] = "เลือกวัน";
+Calendar._TT["DRAG_TO_MOVE"] = "กดแล้วลากเพื่อย้าย";
+Calendar._TT["PART_TODAY"] = " (วันนี้)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "แสดง %s เป็นวันแรก";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "ปิด";
+Calendar._TT["TODAY"] = "วันนี้";
+Calendar._TT["TIME_PART"] = "(Shift-)กดหรือกดแล้วลากเพื่อเปลี่ยนค่า";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a %e %b";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "เวลา:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Pazar",
+ "Pazartesi",
+ "Salı",
+ "Çarşamba",
+ "Perşembe",
+ "Cuma",
+ "Cumartesi",
+ "Pazar");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Paz",
+ "Pzt",
+ "Sal",
+ "Çar",
+ "Per",
+ "Cum",
+ "Cmt",
+ "Paz");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Ocak",
+ "Şubat",
+ "Mart",
+ "Nisan",
+ "Mayıs",
+ "Haziran",
+ "Temmuz",
+ "Ağustos",
+ "Eylül",
+ "Ekim",
+ "Kasım",
+ "Aralık");
+
+// short month names
+Calendar._SMN = new Array
+("Oca",
+ "Şub",
+ "Mar",
+ "Nis",
+ "May",
+ "Haz",
+ "Tem",
+ "Ağu",
+ "Eyl",
+ "Eki",
+ "Kas",
+ "Ara");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Takvim hakkında";
+
+Calendar._TT["ABOUT"] =
+"DHTML Tarih/Zaman Seçici\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Tarih Seçimi:\n" +
+"- Yıl seçmek için \xab, \xbb tuşlarını kullanın\n" +
+"- Ayı seçmek için " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tuşlarını kullanın\n" +
+"- Hızlı seçim için yukardaki butonların üzerinde farenin tuşuna basılı tutun.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Zaman Seçimi:\n" +
+"- Arttırmak için herhangi bir zaman bölümüne tıklayın\n" +
+"- ya da azaltmak için Shift+tıkla yapın\n" +
+"- ya da daha hızlı bir seçim için tıklayın ve sürükleyin.";
+
+Calendar._TT["PREV_YEAR"] = "Öncki yıl (Menu için basılı tutun)";
+Calendar._TT["PREV_MONTH"] = "Önceki ay (Menu için basılı tutun)";
+Calendar._TT["GO_TODAY"] = "Bugüne Git";
+Calendar._TT["NEXT_MONTH"] = "Sonraki Ay (Menu için basılı tutun)";
+Calendar._TT["NEXT_YEAR"] = "Next year (Menu için basılı tutun)";
+Calendar._TT["SEL_DATE"] = "Tarih seçin";
+Calendar._TT["DRAG_TO_MOVE"] = "Taşımak için sürükleyin";
+Calendar._TT["PART_TODAY"] = " (bugün)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "%s : önce göster";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "1,0";
+
+Calendar._TT["CLOSE"] = "Kapat";
+Calendar._TT["TODAY"] = "Bugün";
+Calendar._TT["TIME_PART"] = "Değeri değiştirmek için (Shift-)tıkla veya sürükle";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "Hafta";
+Calendar._TT["TIME"] = "Saat:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "About the calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Go Today";
+Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Select date";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
+Calendar._TT["PART_TODAY"] = " (today)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Display %s first";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Close";
+Calendar._TT["TODAY"] = "Today";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Time:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Chủ nhật",
+ "Thứ Hai",
+ "Thứ Ba",
+ "Thứ Tư",
+ "Thứ Năm",
+ "Thứ Sáu",
+ "Thứ Bảy",
+ "Chủ Nhật");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("C.Nhật",
+ "Hai",
+ "Ba",
+ "Tư",
+ "Năm",
+ "Sáu",
+ "Bảy",
+ "C.Nhật");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Tháng Giêng",
+ "Tháng Hai",
+ "Tháng Ba",
+ "Tháng Tư",
+ "Tháng Năm",
+ "Tháng Sáu",
+ "Tháng Bảy",
+ "Tháng Tám",
+ "Tháng Chín",
+ "Tháng Mười",
+ "Tháng M.Một",
+ "Tháng Chạp");
+
+// short month names
+Calendar._SMN = new Array
+("Mmột",
+ "Hai",
+ "Ba",
+ "Tư",
+ "Năm",
+ "Sáu",
+ "Bảy",
+ "Tám",
+ "Chín",
+ "Mười",
+ "MMột",
+ "Chạp");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "Giới thiệu";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector (c) dynarch.com 2002-2005 / Tác giả: Mihai Bazon. " + // don't translate this this ;-)
+"Phiên bản mới nhất có tại: http://www.dynarch.com/projects/calendar/. " +
+"Sản phẩm được phân phối theo giấy phép GNU LGPL. Xem chi tiết tại http://gnu.org/licenses/lgpl.html." +
+"\n\n" +
+"Chọn ngày:\n" +
+"- Dùng nút \xab, \xbb để chọn năm\n" +
+"- Dùng nút " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " để chọn tháng\n" +
+"- Giữ chuột vào các nút trên để có danh sách năm và tháng.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Chọn thời gian:\n" +
+"- Click chuột trên từng phần của thời gian để chỉnh sửa\n" +
+"- hoặc nhấn Shift + click chuột để tăng giá trị\n" +
+"- hoặc click chuột và kéo (drag) để chọn nhanh.";
+
+Calendar._TT["PREV_YEAR"] = "Năm trước (giữ chuột để có menu)";
+Calendar._TT["PREV_MONTH"] = "Tháng trước (giữ chuột để có menu)";
+Calendar._TT["GO_TODAY"] = "đến Hôm nay";
+Calendar._TT["NEXT_MONTH"] = "Tháng tới (giữ chuột để có menu)";
+Calendar._TT["NEXT_YEAR"] = "Ngày tới (giữ chuột để có menu)";
+Calendar._TT["SEL_DATE"] = "Chọn ngày";
+Calendar._TT["DRAG_TO_MOVE"] = "Kéo (drag) để di chuyển";
+Calendar._TT["PART_TODAY"] = " (hôm nay)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Hiển thị %s trước";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Đóng";
+Calendar._TT["TODAY"] = "Hôm nay";
+Calendar._TT["TIME_PART"] = "Click, shift-click hoặc kéo (drag) để đổi giá trị";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Time:";
--- /dev/null
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("星期日",
+ "星期一",
+ "星期二",
+ "星期三",
+ "星期四",
+ "星期五",
+ "星期六",
+ "星期日");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("日",
+ "一",
+ "二",
+ "三",
+ "四",
+ "五",
+ "六",
+ "日");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("一月",
+ "二月",
+ "三月",
+ "四月",
+ "五月",
+ "六月",
+ "七月",
+ "八月",
+ "九月",
+ "十月",
+ "十一月",
+ "十二月");
+
+// short month names
+Calendar._SMN = new Array
+("一月",
+ "二月",
+ "三月",
+ "四月",
+ "五月",
+ "六月",
+ "七月",
+ "八月",
+ "九月",
+ "十月",
+ "十一月",
+ "十二月");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "關於 calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML 日期/時間 選擇器\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"最新版本取得位址: http://www.dynarch.com/projects/calendar/\n" +
+"使用 GNU LGPL 發行. 參考 http://gnu.org/licenses/lgpl.html 以取得更多關於 LGPL 之細節。" +
+"\n\n" +
+"日期選擇方式:\n" +
+"- 使用滑鼠點擊 \xab 、 \xbb 按鈕選擇年份\n" +
+"- 使用滑鼠點擊 " + String.fromCharCode(0x2039) + " 、 " + String.fromCharCode(0x203a) + " 按鈕選擇月份\n" +
+"- 使用滑鼠點擊上述按鈕並按住不放,可開啟快速選單。";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"時間選擇方式:\n" +
+"- 「單擊」時分秒為遞增\n" +
+"- 或 「Shift-單擊」為遞減\n" +
+"- 或 「單擊且拖拉」為快速選擇";
+
+Calendar._TT["PREV_YEAR"] = "前一年 (按住不放可顯示選單)";
+Calendar._TT["PREV_MONTH"] = "前一個月 (按住不放可顯示選單)";
+Calendar._TT["GO_TODAY"] = "選擇今天";
+Calendar._TT["NEXT_MONTH"] = "後一個月 (按住不放可顯示選單)";
+Calendar._TT["NEXT_YEAR"] = "下一年 (按住不放可顯式選單)";
+Calendar._TT["SEL_DATE"] = "請點選日期";
+Calendar._TT["DRAG_TO_MOVE"] = "按住不放可拖拉視窗";
+Calendar._TT["PART_TODAY"] = " (今天)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "以 %s 做為一週的首日";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "關閉視窗";
+Calendar._TT["TODAY"] = "今天";
+Calendar._TT["TIME_PART"] = "(Shift-)加「單擊」或「拖拉」可變更值";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "星期 %a, %b %e 日";
+
+Calendar._TT["WK"] = "週";
+Calendar._TT["TIME"] = "時間:";
--- /dev/null
+// ** I18N
+
+// Calendar Chinese language
+// Author: Andy Wu, <andywu.zh@gmail.com>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("星期日",
+ "星期一",
+ "星期二",
+ "星期三",
+ "星期四",
+ "星期五",
+ "星期六",
+ "星期日");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("日",
+ "一",
+ "二",
+ "三",
+ "四",
+ "五",
+ "六",
+ "日");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// full month names
+Calendar._MN = new Array
+("1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月");
+
+// short month names
+Calendar._SMN = new Array
+("1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "关于日历";
+
+Calendar._TT["ABOUT"] =
+"DHTML 日期/时间 选择器\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"最新版本请访问: http://www.dynarch.com/projects/calendar/\n" +
+"遵循 GNU LGPL 发布。详情请查阅 http://gnu.org/licenses/lgpl.html " +
+"\n\n" +
+"日期选择:\n" +
+"- 使用 \xab,\xbb 按钮选择年\n" +
+"- 使用 " + String.fromCharCode(0x2039) + "," + String.fromCharCode(0x203a) + " 按钮选择月\n" +
+"- 在上述按钮上按住不放可以快速选择";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"时间选择:\n" +
+"- 点击时间的任意部分来增加\n" +
+"- Shift加点击来减少\n" +
+"- 点击后拖动进行快速选择";
+
+Calendar._TT["PREV_YEAR"] = "上年(按住不放显示菜单)";
+Calendar._TT["PREV_MONTH"] = "上月(按住不放显示菜单)";
+Calendar._TT["GO_TODAY"] = "回到今天";
+Calendar._TT["NEXT_MONTH"] = "下月(按住不放显示菜单)";
+Calendar._TT["NEXT_YEAR"] = "下年(按住不放显示菜单)";
+Calendar._TT["SEL_DATE"] = "选择日期";
+Calendar._TT["DRAG_TO_MOVE"] = "拖动";
+Calendar._TT["PART_TODAY"] = " (今日)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "一周开始于 %s";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "关闭";
+Calendar._TT["TODAY"] = "今天";
+Calendar._TT["TIME_PART"] = "Shift加点击或者拖动来变更";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "星期%a %b%e日";
+
+Calendar._TT["WK"] = "周";
+Calendar._TT["TIME"] = "时间:";
--- /dev/null
+/* redMine - project management software
+ Copyright (C) 2006-2008 Jean-Philippe Lang */
+
+var observingContextMenuClick;
+
+ContextMenu = Class.create();
+ContextMenu.prototype = {
+ initialize: function (url) {
+ this.url = url;
+
+ // prevent text selection in the issue list
+ var tables = $$('table.issues');
+ for (i=0; i<tables.length; i++) {
+ tables[i].onselectstart = function () { return false; } // ie
+ tables[i].onmousedown = function () { return false; } // mozilla
+ }
+
+ if (!observingContextMenuClick) {
+ Event.observe(document, 'click', this.Click.bindAsEventListener(this));
+ Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
+ observingContextMenuClick = true;
+ }
+
+ this.unselectAll();
+ this.lastSelected = null;
+ },
+
+ RightClick: function(e) {
+ this.hideMenu();
+ // do not show the context menu on links
+ if (Event.element(e).tagName == 'A') { return; }
+ // right-click simulated by Alt+Click with Opera
+ if (window.opera && !e.altKey) { return; }
+ var tr = Event.findElement(e, 'tr');
+ if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; }
+ Event.stop(e);
+ if (!this.isSelected(tr)) {
+ this.unselectAll();
+ this.addSelection(tr);
+ this.lastSelected = tr;
+ }
+ this.showMenu(e);
+ },
+
+ Click: function(e) {
+ this.hideMenu();
+ if (Event.element(e).tagName == 'A') { return; }
+ if (window.opera && e.altKey) { return; }
+ if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
+ var tr = Event.findElement(e, 'tr');
+ if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) {
+ // a row was clicked, check if the click was on checkbox
+ var box = Event.findElement(e, 'input');
+ if (box!=document && box!=undefined) {
+ // a checkbox may be clicked
+ if (box.checked) {
+ tr.addClassName('context-menu-selection');
+ } else {
+ tr.removeClassName('context-menu-selection');
+ }
+ } else {
+ if (e.ctrlKey) {
+ this.toggleSelection(tr);
+ } else if (e.shiftKey) {
+ if (this.lastSelected != null) {
+ var toggling = false;
+ var rows = $$('.hascontextmenu');
+ for (i=0; i<rows.length; i++) {
+ if (toggling || rows[i]==tr) {
+ this.addSelection(rows[i]);
+ }
+ if (rows[i]==tr || rows[i]==this.lastSelected) {
+ toggling = !toggling;
+ }
+ }
+ } else {
+ this.addSelection(tr);
+ }
+ } else {
+ this.unselectAll();
+ this.addSelection(tr);
+ }
+ this.lastSelected = tr;
+ }
+ } else {
+ // click is outside the rows
+ var t = Event.findElement(e, 'a');
+ if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
+ Event.stop(e);
+ }
+ }
+ }
+ else{
+ this.RightClick(e);
+ }
+ },
+
+ showMenu: function(e) {
+ var mouse_x = Event.pointerX(e);
+ var mouse_y = Event.pointerY(e);
+ var render_x = mouse_x;
+ var render_y = mouse_y;
+ var dims;
+ var menu_width;
+ var menu_height;
+ var window_width;
+ var window_height;
+ var max_width;
+ var max_height;
+
+ $('context-menu').style['left'] = (render_x + 'px');
+ $('context-menu').style['top'] = (render_y + 'px');
+ Element.update('context-menu', '');
+
+ new Ajax.Updater({success:'context-menu'}, this.url,
+ {asynchronous:true,
+ evalScripts:true,
+ parameters:Form.serialize(Event.findElement(e, 'form')),
+ onComplete:function(request){
+ dims = $('context-menu').getDimensions();
+ menu_width = dims.width;
+ menu_height = dims.height;
+ max_width = mouse_x + 2*menu_width;
+ max_height = mouse_y + menu_height;
+
+ var ws = window_size();
+ window_width = ws.width;
+ window_height = ws.height;
+
+ /* display the menu above and/or to the left of the click if needed */
+ if (max_width > window_width) {
+ render_x -= menu_width;
+ $('context-menu').addClassName('reverse-x');
+ } else {
+ $('context-menu').removeClassName('reverse-x');
+ }
+ if (max_height > window_height) {
+ render_y -= menu_height;
+ $('context-menu').addClassName('reverse-y');
+ } else {
+ $('context-menu').removeClassName('reverse-y');
+ }
+ if (render_x <= 0) render_x = 1;
+ if (render_y <= 0) render_y = 1;
+ $('context-menu').style['left'] = (render_x + 'px');
+ $('context-menu').style['top'] = (render_y + 'px');
+
+ Effect.Appear('context-menu', {duration: 0.20});
+ if (window.parseStylesheets) { window.parseStylesheets(); } // IE
+ }})
+ },
+
+ hideMenu: function() {
+ Element.hide('context-menu');
+ },
+
+ addSelection: function(tr) {
+ tr.addClassName('context-menu-selection');
+ this.checkSelectionBox(tr, true);
+ },
+
+ toggleSelection: function(tr) {
+ if (this.isSelected(tr)) {
+ this.removeSelection(tr);
+ } else {
+ this.addSelection(tr);
+ }
+ },
+
+ removeSelection: function(tr) {
+ tr.removeClassName('context-menu-selection');
+ this.checkSelectionBox(tr, false);
+ },
+
+ unselectAll: function() {
+ var rows = $$('.hascontextmenu');
+ for (i=0; i<rows.length; i++) {
+ this.removeSelection(rows[i]);
+ }
+ },
+
+ checkSelectionBox: function(tr, checked) {
+ var inputs = Element.getElementsBySelector(tr, 'input');
+ if (inputs.length > 0) { inputs[0].checked = checked; }
+ },
+
+ isSelected: function(tr) {
+ return Element.hasClassName(tr, 'context-menu-selection');
+ }
+}
+
+function toggleIssuesSelection(el) {
+ var boxes = el.getElementsBySelector('input[type=checkbox]');
+ var all_checked = true;
+ for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
+ for (i = 0; i < boxes.length; i++) {
+ if (all_checked) {
+ boxes[i].checked = false;
+ boxes[i].up('tr').removeClassName('context-menu-selection');
+ } else if (boxes[i].checked == false) {
+ boxes[i].checked = true;
+ boxes[i].up('tr').addClassName('context-menu-selection');
+ }
+ }
+}
+
+function window_size() {
+ var w;
+ var h;
+ if (window.innerWidth) {
+ w = window.innerWidth;
+ h = window.innerHeight;
+ } else if (document.documentElement) {
+ w = document.documentElement.clientWidth;
+ h = document.documentElement.clientHeight;
+ } else {
+ w = document.body.clientWidth;
+ h = document.body.clientHeight;
+ }
+ return {width: w, height: h};
+}
--- /dev/null
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || { };
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {
+ setHeight: false,
+ offsetTop: element.offsetHeight
+ });
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--;
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++;
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.value;
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+};
+
+Ajax.InPlaceEditor = Class.create({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML.unescapeHTML();
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
+ }
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
+ },
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
+ showSaving: function() {
+ this._oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ },
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
+ }
+ },
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
+ },
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw('Server returned an invalid collection representation.');
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadCollectionURL, options);
+ },
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
+ }
+ tempOption.update((text || '').stripScripts().stripTags());
+ },
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
+ },
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
+ }
+});
+
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
+
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
+ }
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
+ }
+});
+
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create({
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;
+ if(this.timer) clearTimeout(this.timer);
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+ this.lastValue = $F(this.element);
+ },
+ onTimerEvent: function() {
+ this.timer = null;
+ this.callback(this.element, $F(this.element));
+ }
+});
\ No newline at end of file
--- /dev/null
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(Object.isUndefined(Effect))
+ throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+var Droppables = {
+ drops: [],
+
+ remove: function(element) {
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+ },
+
+ add: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null,
+ tree: false
+ }, arguments[1] || { });
+
+ // cache containers
+ if(options.containment) {
+ options._containers = [];
+ var containment = options.containment;
+ if(Object.isArray(containment)) {
+ containment.each( function(c) { options._containers.push($(c)) });
+ } else {
+ options._containers.push($(containment));
+ }
+ }
+
+ if(options.accept) options.accept = [options.accept].flatten();
+
+ Element.makePositioned(element); // fix IE
+ options.element = element;
+
+ this.drops.push(options);
+ },
+
+ findDeepestChild: function(drops) {
+ deepest = drops[0];
+
+ for (i = 1; i < drops.length; ++i)
+ if (Element.isParent(drops[i].element, deepest.element))
+ deepest = drops[i];
+
+ return deepest;
+ },
+
+ isContained: function(element, drop) {
+ var containmentNode;
+ if(drop.tree) {
+ containmentNode = element.treeNode;
+ } else {
+ containmentNode = element.parentNode;
+ }
+ return drop._containers.detect(function(c) { return containmentNode == c });
+ },
+
+ isAffected: function(point, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ Position.within(drop.element, point[0], point[1]) );
+ },
+
+ deactivate: function(drop) {
+ if(drop.hoverclass)
+ Element.removeClassName(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(drop.hoverclass)
+ Element.addClassName(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ },
+
+ show: function(point, element) {
+ if(!this.drops.length) return;
+ var drop, affected = [];
+
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0)
+ drop = Droppables.findDeepestChild(affected);
+
+ if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
+ if (drop) {
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ if (drop != this.last_active) Droppables.activate(drop);
+ }
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop) {
+ this.last_active.onDrop(element, this.last_active.element, event);
+ return true;
+ }
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+};
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ if(draggable.options.delay) {
+ this._timeout = setTimeout(function() {
+ Draggables._timeout = null;
+ window.focus();
+ Draggables.activeDraggable = draggable;
+ }.bind(this), draggable.options.delay);
+ } else {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ }
+ },
+
+ deactivate: function() {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ this.activeDraggable = null;
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create({
+ initialize: function(element) {
+ var defaults = {
+ handle: false,
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+ queue: {scope:'_draggable', position:'end'}
+ });
+ },
+ endeffect: function(element) {
+ var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
+ queue: {scope:'_draggable', position:'end'},
+ afterFinish: function(){
+ Draggable._dragging[element] = false
+ }
+ });
+ },
+ zindex: 1000,
+ revert: false,
+ quiet: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ delay: 0
+ };
+
+ if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
+ Object.extend(defaults, {
+ starteffect: function(element) {
+ element._opacity = Element.getOpacity(element);
+ Draggable._dragging[element] = true;
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
+ }
+ });
+
+ var options = Object.extend(defaults, arguments[1] || { });
+
+ this.element = $(element);
+
+ if(options.handle && Object.isString(options.handle))
+ this.handle = this.element.down('.'+options.handle, 0);
+
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+ options.scroll = $(options.scroll);
+ this._isScrollChild = Element.childOf(this.element, options.scroll);
+ }
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(Element.getStyle(this.element,'left') || '0'),
+ parseInt(Element.getStyle(this.element,'top') || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(!Object.isUndefined(Draggable._dragging[this.element]) &&
+ Draggable._dragging[this.element]) return;
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if((tag_name = src.tagName.toUpperCase()) && (
+ tag_name=='INPUT' ||
+ tag_name=='SELECT' ||
+ tag_name=='OPTION' ||
+ tag_name=='BUTTON' ||
+ tag_name=='TEXTAREA')) return;
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+ if(!this.delta)
+ this.delta = this.currentDelta();
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
+ if (!this._originallyAbsolute)
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ if(this.options.scroll) {
+ if (this.options.scroll == window) {
+ var where = this._getWindowScroll(this.options.scroll);
+ this.originalScrollLeft = where.left;
+ this.originalScrollTop = where.top;
+ } else {
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
+ this.originalScrollTop = this.options.scroll.scrollTop;
+ }
+ }
+
+ Draggables.notify('onStart', this, event);
+
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+
+ if(!this.options.quiet){
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ }
+
+ Draggables.notify('onDrag', this, event);
+
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ if(this.options.scroll) {
+ this.stopScrolling();
+
+ var p;
+ if (this.options.scroll == window) {
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+ } else {
+ p = Position.page(this.options.scroll);
+ p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+ p[1] += this.options.scroll.scrollTop + Position.deltaY;
+ p.push(p[0]+this.options.scroll.offsetWidth);
+ p.push(p[1]+this.options.scroll.offsetHeight);
+ }
+ var speed = [0,0];
+ if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(Prototype.Browser.WebKit) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.quiet){
+ Position.prepare();
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ Droppables.show(pointer, this.element);
+ }
+
+ if(this.options.ghosting) {
+ if (!this._originallyAbsolute)
+ Position.relativize(this.element);
+ delete this._originallyAbsolute;
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ var dropped = false;
+ if(success) {
+ dropped = Droppables.fire(event, this.element);
+ if (!dropped) dropped = false;
+ }
+ if(dropped && this.options.onDropped) this.options.onDropped(this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && Object.isFunction(revert)) revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ if (dropped == 0 || revert != 'failure')
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(event.keyCode!=Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.stopScrolling();
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ if(this.options.ghosting) {
+ var r = Position.realOffset(this.element);
+ pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+ }
+
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(Object.isFunction(this.options.snap)) {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(Object.isArray(this.options.snap)) {
+ p = p.map( function(v, i) {
+ return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
+ } else {
+ p = p.map( function(v) {
+ return (v/this.options.snap).round()*this.options.snap }.bind(this));
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+
+ stopScrolling: function() {
+ if(this.scrollInterval) {
+ clearInterval(this.scrollInterval);
+ this.scrollInterval = null;
+ Draggables._lastScrollPointer = null;
+ }
+ },
+
+ startScrolling: function(speed) {
+ if(!(speed[0] || speed[1])) return;
+ this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+ this.lastScrolled = new Date();
+ this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+ },
+
+ scroll: function() {
+ var current = new Date();
+ var delta = current - this.lastScrolled;
+ this.lastScrolled = current;
+ if(this.options.scroll == window) {
+ with (this._getWindowScroll(this.options.scroll)) {
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+ var d = delta / 1000;
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+ }
+ }
+ } else {
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
+ }
+
+ Position.prepare();
+ Droppables.show(Draggables._lastPointer, this.element);
+ Draggables.notify('onDrag', this);
+ if (this._isScrollChild) {
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+ if (Draggables._lastScrollPointer[0] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 0)
+ Draggables._lastScrollPointer[1] = 0;
+ this.draw(Draggables._lastScrollPointer);
+ }
+
+ if(this.options.change) this.options.change(this);
+ },
+
+ _getWindowScroll: function(w) {
+ var T, L, W, H;
+ with (w.document) {
+ if (w.document.documentElement && documentElement.scrollTop) {
+ T = documentElement.scrollTop;
+ L = documentElement.scrollLeft;
+ } else if (w.document.body) {
+ T = body.scrollTop;
+ L = body.scrollLeft;
+ }
+ if (w.innerWidth) {
+ W = w.innerWidth;
+ H = w.innerHeight;
+ } else if (w.document.documentElement && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight;
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+});
+
+Draggable._dragging = { };
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create({
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+});
+
+var Sortable = {
+ SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+
+ sortables: { },
+
+ _findRootElement: function(element) {
+ while (element.tagName.toUpperCase() != "BODY") {
+ if(element.id && Sortable.sortables[element.id]) return element;
+ element = element.parentNode;
+ }
+ },
+
+ options: function(element) {
+ element = Sortable._findRootElement($(element));
+ if(!element) return;
+ return Sortable.sortables[element.id];
+ },
+
+ destroy: function(element){
+ element = $(element);
+ var s = Sortable.sortables[element.id];
+
+ if(s) {
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+
+ delete Sortable.sortables[s.element.id];
+ }
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false,
+ treeTag: 'ul',
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ delay: 0,
+ hoverclass: null,
+ ghosting: false,
+ quiet: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: this.SERIALIZE_RULE,
+
+ // these take arrays of elements or ids and can be
+ // used for better initialization performance
+ elements: false,
+ handles: false,
+
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || { });
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ quiet: options.quiet,
+ scroll: options.scroll,
+ scrollSpeed: options.scrollSpeed,
+ scrollSensitivity: options.scrollSensitivity,
+ delay: options.delay,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ tree: options.tree,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover
+ };
+
+ var options_for_tree = {
+ onHover: Sortable.onEmptyHover,
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass
+ };
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // drop on empty handling
+ if(options.dropOnEmpty || options.tree) {
+ Droppables.add(element, options_for_tree);
+ options.droppables.push(element);
+ }
+
+ (options.elements || this.findElements(element, options) || []).each( function(e,i) {
+ var handle = options.handles ? $(options.handles[i]) :
+ (options.handle ? $(e).select('.' + options.handle)[0] : e);
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ if(options.tree) e.treeNode = element;
+ options.droppables.push(e);
+ });
+
+ if(options.tree) {
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
+ Droppables.add(e, options_for_tree);
+ e.treeNode = element;
+ options.droppables.push(e);
+ });
+ }
+
+ // keep reference
+ this.sortables[element.id] = options;
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.tag);
+ },
+
+ findTreeElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.treeTag);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(Element.isParent(dropon, element)) return;
+
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, overlap) {
+ var oldParentNode = element.parentNode;
+ var droponOptions = Sortable.options(dropon);
+
+ if(!Element.isParent(dropon, element)) {
+ var index;
+
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+ var child = null;
+
+ if(children) {
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+
+ for (index = 0; index < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < children.length ? children[index + 1] : null;
+ break;
+ } else {
+ child = children[index];
+ break;
+ }
+ }
+ }
+
+ dropon.insertBefore(element, child);
+
+ Sortable.options(oldParentNode).onChange(element);
+ droponOptions.onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Sortable._marker.hide();
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker =
+ ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+ hide().addClassName('dropmarker').setStyle({position:'absolute'});
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+ else
+ Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+
+ Sortable._marker.show();
+ },
+
+ _tree: function(element, options, parent) {
+ var children = Sortable.findElements(element, options) || [];
+
+ for (var i = 0; i < children.length; ++i) {
+ var match = children[i].id.match(options.format);
+
+ if (!match) continue;
+
+ var child = {
+ id: encodeURIComponent(match ? match[1] : null),
+ element: element,
+ parent: parent,
+ children: [],
+ position: parent.children.length,
+ container: $(children[i]).down(options.treeTag)
+ };
+
+ /* Get the element containing the children and recurse over it */
+ if (child.container)
+ this._tree(child.container, options, child);
+
+ parent.children.push (child);
+ }
+
+ return parent;
+ },
+
+ tree: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ treeTag: sortableOptions.treeTag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format
+ }, arguments[1] || { });
+
+ var root = {
+ id: null,
+ parent: null,
+ children: [],
+ container: element,
+ position: 0
+ };
+
+ return Sortable._tree(element, options, root);
+ },
+
+ /* Construct a [i] index for a particular node */
+ _constructIndex: function(node) {
+ var index = '';
+ do {
+ if (node.id) index = '[' + node.position + ']' + index;
+ } while ((node = node.parent) != null);
+ return index;
+ },
+
+ sequence: function(element) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[1] || { });
+
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+ });
+ },
+
+ setSequence: function(element, new_sequence) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[2] || { });
+
+ var nodeMap = { };
+ this.findElements(element, options).each( function(n) {
+ if (n.id.match(options.format))
+ nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+ n.parentNode.removeChild(n);
+ });
+
+ new_sequence.each(function(ident) {
+ var n = nodeMap[ident];
+ if (n) {
+ n[1].appendChild(n[0]);
+ delete nodeMap[ident];
+ }
+ });
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var options = Object.extend(Sortable.options(element), arguments[1] || { });
+ var name = encodeURIComponent(
+ (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+
+ if (options.tree) {
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
+ return [name + Sortable._constructIndex(item) + "[id]=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+};
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+ if (!child.parentNode || child == element) return false;
+ if (child.parentNode == element) return true;
+ return Element.isParent(child.parentNode, element);
+};
+
+Element.findChildren = function(element, only, recursive, tagName) {
+ if(!element.hasChildNodes()) return null;
+ tagName = tagName.toUpperCase();
+ if(only) only = [only].flatten();
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+ elements.push(e);
+ if(recursive) {
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : []);
+};
+
+Element.offsetSize = function (element, type) {
+ return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+};
\ No newline at end of file
--- /dev/null
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if (this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if (this.slice(0,1) == '#') {
+ if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if (this.length==7) color = this.toLowerCase();
+ }
+ }
+ return (color.length==7 ? color : (arguments[0] || this));
+};
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+};
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+};
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if (Prototype.Browser.WebKit) window.scrollBy(0,0);
+ return element;
+};
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+};
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ Transitions: {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + .5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
+ return pos > 1 ? 1 : pos;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
+ },
+ pulse: function(pos, pulses) {
+ return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
+ },
+ spring: function(pos) {
+ return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+ },
+ DefaultOptions: {
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ },
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if (child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ new Element('span', {style: tagifyStyle}).update(
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if (((typeof element == 'object') ||
+ Object.isFunction(element)) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || { });
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || { });
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create(Enumerable, {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = Object.isString(effect.options.queue) ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if (!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if (this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ for(var i=0, len=this.effects.length;i<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if (!Object.isString(queueName)) return queueName;
+
+ return this.instances.get(queueName) ||
+ this.instances.set(queueName, new Effect.ScopedQueue());
+ }
+};
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.Base = Class.create({
+ position: null,
+ start: function(options) {
+ function codeForEvent(options,eventName){
+ return (
+ (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+ (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+ );
+ }
+ if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+ this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn+(this.options.duration*1000);
+ this.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ this.render = (function() {
+ function dispatch(effect, eventName) {
+ if (effect.options[eventName + 'Internal'])
+ effect.options[eventName + 'Internal'](effect);
+ if (effect.options[eventName])
+ effect.options[eventName](effect);
+ }
+
+ return function(pos) {
+ if (this.state === "idle") {
+ this.state = "running";
+ dispatch(this, 'beforeSetup');
+ if (this.setup) this.setup();
+ dispatch(this, 'afterSetup');
+ }
+ if (this.state === "running") {
+ pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
+ this.position = pos;
+ dispatch(this, 'beforeUpdate');
+ if (this.update) this.update(pos);
+ dispatch(this, 'afterUpdate');
+ }
+ };
+ })();
+
+ this.event('beforeStart');
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if (timePos >= this.startOn) {
+ if (timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if (this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = (pos * this.totalFrames).round();
+ if (frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ cancel: function() {
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if (this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ var data = $H();
+ for(property in this)
+ if (!Object.isFunction(this[property])) data.set(property, this[property]);
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+});
+
+Effect.Parallel = Class.create(Effect.Base, {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if (effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Tween = Class.create(Effect.Base, {
+ initialize: function(object, from, to) {
+ object = Object.isString(object) ? $(object) : object;
+ var args = $A(arguments), method = args.last(),
+ options = args.length == 5 ? args[3] : null;
+ this.method = Object.isFunction(method) ? method.bind(object) :
+ Object.isFunction(object[method]) ? object[method].bind(object) :
+ function(value) { object[method] = value };
+ this.start(Object.extend({ from: from, to: to }, options || { }));
+ },
+ update: function(position) {
+ this.method(position);
+ }
+});
+
+Effect.Event = Class.create(Effect.Base, {
+ initialize: function() {
+ this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if (this.options.mode == 'absolute') {
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: (this.options.x * position + this.originalLeft).round() + 'px',
+ top: (this.options.y * position + this.originalTop).round() + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
+};
+
+Effect.Scale = Class.create(Effect.Base, {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or { } with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = { };
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if (fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if (this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if (/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if (!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if (this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = { };
+ if (this.options.scaleX) d.width = width.round() + 'px';
+ if (this.options.scaleY) d.height = height.round() + 'px';
+ if (this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if (this.elementPositioning == 'absolute') {
+ if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if (this.options.scaleY) d.top = -topd + 'px';
+ if (this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if (this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = { };
+ if (!this.options.keepBackgroundImage) {
+ this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+ this.element.setStyle({backgroundImage: 'none'});
+ }
+ if (!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if (!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = function(element) {
+ var options = arguments[1] || { },
+ scrollOffsets = document.viewport.getScrollOffsets(),
+ elementOffsets = $(element).cumulativeOffset();
+
+ if (options.offset) elementOffsets[1] += options.offset;
+
+ return new Effect.Tween(null,
+ scrollOffsets.top,
+ elementOffsets[1],
+ options,
+ function(p){ scrollTo(scrollOffsets.left, p.round()); }
+ );
+};
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if (effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }
+ }, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element);
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ });
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ distance: 20,
+ duration: 0.5
+ }, arguments[1] || {});
+ var distance = parseFloat(options.distance);
+ var split = parseFloat(options.duration) / 10.0;
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}); }}); }}); }}); }}); }});
+};
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
+ }
+ }, arguments[1] || { })
+ );
+};
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+};
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ );
+ }
+ });
+};
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+};
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || { },
+ oldOpacity = element.getInlineOpacity(),
+ transition = options.transition || Effect.Transitions.linear,
+ reverser = function(pos){
+ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
+ };
+
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+};
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || { }));
+};
+
+Effect.Morph = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: { }
+ }, arguments[1] || { });
+
+ if (!Object.isString(options.style)) this.style = $H(options.style);
+ else {
+ if (options.style.include(':'))
+ this.style = options.style.parseStyle();
+ else {
+ this.element.addClassName(options.style);
+ this.style = $H(this.element.getStyles());
+ this.element.removeClassName(options.style);
+ var css = this.element.getStyles();
+ this.style = this.style.reject(function(style) {
+ return style.value == css[style.key];
+ });
+ options.afterFinishInternal = function(effect) {
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ effect.element.style[transform.style] = '';
+ });
+ };
+ }
+ }
+ this.start(options);
+ },
+
+ setup: function(){
+ function parseColor(color){
+ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 );
+ });
+ }
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if (property == 'opacity') {
+ value = parseFloat(value);
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if (Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ );
+ });
+ },
+ update: function(position) {
+ var style = { }, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ (transform.originalValue +
+ (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+ (transform.unit === null ? '' : transform.unit);
+ this.element.setStyle(style, true);
+ }
+});
+
+Effect.Transform = Class.create({
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || { };
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ track = $H(track);
+ var data = track.values().first();
+ this.tracks.push($H({
+ ids: track.keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+ var elements = [$(ids) || $$(ids)].flatten();
+ return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.__parseStyleElement = document.createElement('div');
+String.prototype.parseStyle = function(){
+ var style, styleRules = $H();
+ if (Prototype.Browser.WebKit)
+ style = new Element('div',{style:this}).style;
+ else {
+ String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
+ style = String.__parseStyleElement.childNodes[0].style;
+ }
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if (style[property]) styleRules.set(property, style[property]);
+ });
+
+ if (Prototype.Browser.IE && this.include('opacity'))
+ styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+ return styleRules;
+};
+
+if (document.defaultView && document.defaultView.getComputedStyle) {
+ Element.getStyles = function(element) {
+ var css = document.defaultView.getComputedStyle($(element), null);
+ return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+ styles[property] = css[property];
+ return styles;
+ });
+ };
+} else {
+ Element.getStyles = function(element) {
+ element = $(element);
+ var css = element.currentStyle, styles;
+ styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
+ results[property] = css[property];
+ return results;
+ });
+ if (!styles.opacity) styles.opacity = element.getOpacity();
+ return styles;
+ };
+}
+
+Effect.Methods = {
+ morph: function(element, style) {
+ element = $(element);
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+ return element;
+ },
+ visualEffect: function(element, effect, options) {
+ element = $(element);
+ var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[klass](element, options);
+ return element;
+ },
+ highlight: function(element, options) {
+ element = $(element);
+ new Effect.Highlight(element, options);
+ return element;
+ }
+};
+
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+ 'pulsate shake puff squish switchOff dropOut').each(
+ function(effect) {
+ Effect.Methods[effect] = function(element, options){
+ element = $(element);
+ Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+ return element;
+ };
+ }
+);
+
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+ function(f) { Effect.Methods[f] = Element[f]; }
+);
+
+Element.addMethods(Effect.Methods);
\ No newline at end of file
--- /dev/null
+/* ***** BEGIN LICENSE BLOCK *****
+ * This file is part of DotClear.
+ * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
+ * rights reserved.
+ *
+ * DotClear is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * DotClear is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DotClear; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * ***** END LICENSE BLOCK *****
+*/
+
+/* Modified by JP LANG for textile formatting */
+
+function jsToolBar(textarea) {
+ if (!document.createElement) { return; }
+
+ if (!textarea) { return; }
+
+ if ((typeof(document["selection"]) == "undefined")
+ && (typeof(textarea["setSelectionRange"]) == "undefined")) {
+ return;
+ }
+
+ this.textarea = textarea;
+
+ 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);
+
+ // Dragable resizing (only for gecko)
+ if (this.editor.addEventListener)
+ {
+ this.handle = document.createElement('div');
+ this.handle.className = 'jstHandle';
+ var dragStart = this.resizeDragStart;
+ var This = this;
+ this.handle.addEventListener('mousedown',function(event) { dragStart.call(This,event); },false);
+ // fix memory leak in Firefox (bug #241518)
+ window.addEventListener('unload',function() {
+ var del = This.handle.parentNode.removeChild(This.handle);
+ delete(This.handle);
+ },false);
+
+ this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling);
+ }
+
+ this.context = null;
+ this.toolNodes = {}; // lorsque la toolbar est dessinée , cet objet est garni
+ // de raccourcis vers les éléments DOM correspondants aux outils.
+}
+
+function jsButton(title, fn, scope, className) {
+ if(typeof jsToolBar.strings == 'undefined') {
+ this.title = title || null;
+ } else {
+ this.title = jsToolBar.strings[title] || title || null;
+ }
+ this.fn = fn || function(){};
+ this.scope = scope || null;
+ this.className = className || null;
+}
+jsButton.prototype.draw = function() {
+ if (!this.scope) return null;
+
+ var button = document.createElement('button');
+ button.setAttribute('type','button');
+ button.tabIndex = 200;
+ if (this.className) button.className = this.className;
+ button.title = this.title;
+ var span = document.createElement('span');
+ span.appendChild(document.createTextNode(this.title));
+ button.appendChild(span);
+
+ if (this.icon != undefined) {
+ button.style.backgroundImage = 'url('+this.icon+')';
+ }
+ if (typeof(this.fn) == 'function') {
+ var This = this;
+ button.onclick = function() { try { This.fn.apply(This.scope, arguments) } catch (e) {} return false; };
+ }
+ return button;
+}
+
+function jsSpace(id) {
+ this.id = id || null;
+ this.width = null;
+}
+jsSpace.prototype.draw = function() {
+ var span = document.createElement('span');
+ if (this.id) span.id = this.id;
+ span.appendChild(document.createTextNode(String.fromCharCode(160)));
+ span.className = 'jstSpacer';
+ if (this.width) span.style.marginRight = this.width+'px';
+
+ return span;
+}
+
+function jsCombo(title, options, scope, fn, className) {
+ this.title = title || null;
+ this.options = options || null;
+ this.scope = scope || null;
+ this.fn = fn || function(){};
+ this.className = className || null;
+}
+jsCombo.prototype.draw = function() {
+ if (!this.scope || !this.options) return null;
+
+ var select = document.createElement('select');
+ if (this.className) select.className = className;
+ select.title = this.title;
+
+ for (var o in this.options) {
+ //var opt = this.options[o];
+ var option = document.createElement('option');
+ option.value = o;
+ option.appendChild(document.createTextNode(this.options[o]));
+ select.appendChild(option);
+ }
+
+ var This = this;
+ select.onchange = function() {
+ try {
+ This.fn.call(This.scope, this.value);
+ } catch (e) { alert(e); }
+
+ return false;
+ }
+
+ return select;
+}
+
+
+jsToolBar.prototype = {
+ base_url: '',
+ mode: 'wiki',
+ elements: {},
+ help_link: '',
+
+ getMode: function() {
+ return this.mode;
+ },
+
+ setMode: function(mode) {
+ this.mode = mode || 'wiki';
+ },
+
+ switchMode: function(mode) {
+ mode = mode || 'wiki';
+ this.draw(mode);
+ },
+
+ setHelpLink: function(link) {
+ this.help_link = link;
+ },
+
+ button: function(toolName) {
+ var tool = this.elements[toolName];
+ if (typeof tool.fn[this.mode] != 'function') return null;
+ var b = new jsButton(tool.title, tool.fn[this.mode], this, 'jstb_'+toolName);
+ if (tool.icon != undefined) b.icon = tool.icon;
+ return b;
+ },
+ space: function(toolName) {
+ var tool = new jsSpace(toolName)
+ if (this.elements[toolName].width !== undefined)
+ tool.width = this.elements[toolName].width;
+ return tool;
+ },
+ combo: function(toolName) {
+ var tool = this.elements[toolName];
+ var length = tool[this.mode].list.length;
+
+ if (typeof tool[this.mode].fn != 'function' || length == 0) {
+ return null;
+ } else {
+ var options = {};
+ for (var i=0; i < length; i++) {
+ var opt = tool[this.mode].list[i];
+ options[opt] = tool.options[opt];
+ }
+ return new jsCombo(tool.title, options, this, tool[this.mode].fn);
+ }
+ },
+ draw: function(mode) {
+ this.setMode(mode);
+
+ // Empty toolbar
+ while (this.toolbar.hasChildNodes()) {
+ this.toolbar.removeChild(this.toolbar.firstChild)
+ }
+ this.toolNodes = {}; // vide les raccourcis DOM/**/
+
+ var h = document.createElement('div');
+ h.className = 'help'
+ h.innerHTML = this.help_link;
+ '<a href="/help/wiki_syntax.html" onclick="window.open(\'/help/wiki_syntax.html\', \'\', \'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\'); return false;">Aide</a>';
+ this.toolbar.appendChild(h);
+
+ // Draw toolbar elements
+ var b, tool, newTool;
+
+ for (var i in this.elements) {
+ b = this.elements[i];
+
+ var disabled =
+ b.type == undefined || b.type == ''
+ || (b.disabled != undefined && b.disabled)
+ || (b.context != undefined && b.context != null && b.context != this.context);
+
+ if (!disabled && typeof this[b.type] == 'function') {
+ tool = this[b.type](i);
+ if (tool) newTool = tool.draw();
+ if (newTool) {
+ this.toolNodes[i] = newTool; //mémorise l'accès DOM pour usage éventuel ultérieur
+ this.toolbar.appendChild(newTool);
+ }
+ }
+ }
+ },
+
+ singleTag: function(stag,etag) {
+ stag = stag || null;
+ etag = etag || stag;
+
+ if (!stag || !etag) { return; }
+
+ this.encloseSelection(stag,etag);
+ },
+
+ encloseLineSelection: function(prefix, suffix, fn) {
+ this.textarea.focus();
+
+ prefix = prefix || '';
+ suffix = suffix || '';
+
+ var start, end, sel, scrollPos, subst, res;
+
+ if (typeof(document["selection"]) != "undefined") {
+ sel = document.selection.createRange().text;
+ } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
+ start = this.textarea.selectionStart;
+ end = this.textarea.selectionEnd;
+ scrollPos = this.textarea.scrollTop;
+ // go to the start of the line
+ start = this.textarea.value.substring(0, start).replace(/[^\r\n]*$/g,'').length;
+ // go to the end of the line
+ end = this.textarea.value.length - this.textarea.value.substring(end, this.textarea.value.length).replace(/^[^\r\n]*/, '').length;
+ sel = this.textarea.value.substring(start, end);
+ }
+
+ if (sel.match(/ $/)) { // exclude ending space char, if any
+ sel = sel.substring(0, sel.length - 1);
+ suffix = suffix + " ";
+ }
+
+ if (typeof(fn) == 'function') {
+ res = (sel) ? fn.call(this,sel) : fn('');
+ } else {
+ res = (sel) ? sel : '';
+ }
+
+ subst = prefix + res + suffix;
+
+ if (typeof(document["selection"]) != "undefined") {
+ document.selection.createRange().text = subst;
+ var range = this.textarea.createTextRange();
+ range.collapse(false);
+ range.move('character', -suffix.length);
+ range.select();
+ } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
+ this.textarea.value = this.textarea.value.substring(0, start) + subst +
+ this.textarea.value.substring(end);
+ if (sel) {
+ this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+ } else {
+ this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+ }
+ this.textarea.scrollTop = scrollPos;
+ }
+ },
+
+ encloseSelection: function(prefix, suffix, fn) {
+ this.textarea.focus();
+
+ prefix = prefix || '';
+ suffix = suffix || '';
+
+ var start, end, sel, scrollPos, subst, res;
+
+ if (typeof(document["selection"]) != "undefined") {
+ sel = document.selection.createRange().text;
+ } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
+ start = this.textarea.selectionStart;
+ end = this.textarea.selectionEnd;
+ scrollPos = this.textarea.scrollTop;
+ sel = this.textarea.value.substring(start, end);
+ }
+
+ if (sel.match(/ $/)) { // exclude ending space char, if any
+ sel = sel.substring(0, sel.length - 1);
+ suffix = suffix + " ";
+ }
+
+ if (typeof(fn) == 'function') {
+ res = (sel) ? fn.call(this,sel) : fn('');
+ } else {
+ res = (sel) ? sel : '';
+ }
+
+ subst = prefix + res + suffix;
+
+ if (typeof(document["selection"]) != "undefined") {
+ document.selection.createRange().text = subst;
+ var range = this.textarea.createTextRange();
+ range.collapse(false);
+ range.move('character', -suffix.length);
+ range.select();
+// this.textarea.caretPos -= suffix.length;
+ } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
+ this.textarea.value = this.textarea.value.substring(0, start) + subst +
+ this.textarea.value.substring(end);
+ if (sel) {
+ this.textarea.setSelectionRange(start + subst.length, start + subst.length);
+ } else {
+ this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+ }
+ this.textarea.scrollTop = scrollPos;
+ }
+ },
+
+ stripBaseURL: function(url) {
+ if (this.base_url != '') {
+ var pos = url.indexOf(this.base_url);
+ if (pos == 0) {
+ url = url.substr(this.base_url.length);
+ }
+ }
+
+ return url;
+ }
+};
+
+/** Resizer
+-------------------------------------------------------- */
+jsToolBar.prototype.resizeSetStartH = function() {
+ this.dragStartH = this.textarea.offsetHeight + 0;
+};
+jsToolBar.prototype.resizeDragStart = function(event) {
+ var This = this;
+ this.dragStartY = event.clientY;
+ this.resizeSetStartH();
+ document.addEventListener('mousemove', this.dragMoveHdlr=function(event){This.resizeDragMove(event);}, false);
+ document.addEventListener('mouseup', this.dragStopHdlr=function(event){This.resizeDragStop(event);}, false);
+};
+
+jsToolBar.prototype.resizeDragMove = function(event) {
+ this.textarea.style.height = (this.dragStartH+event.clientY-this.dragStartY)+'px';
+};
+
+jsToolBar.prototype.resizeDragStop = function(event) {
+ document.removeEventListener('mousemove', this.dragMoveHdlr, false);
+ document.removeEventListener('mouseup', this.dragStopHdlr, false);
+};
--- /dev/null
+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';
--- /dev/null
+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['Preformatted text'] = 'Preformatted text';
+jsToolBar.strings['Wiki link'] = 'Link na Wiki stranicu';
+jsToolBar.strings['Image'] = 'Slika';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Negreta';
+jsToolBar.strings['Italic'] = 'Cursiva';
+jsToolBar.strings['Underline'] = 'Subratllat';
+jsToolBar.strings['Deleted'] = 'Barrat';
+jsToolBar.strings['Code'] = 'Codi en línia';
+jsToolBar.strings['Heading 1'] = 'Encapçalament 1';
+jsToolBar.strings['Heading 2'] = 'Encapçalament 2';
+jsToolBar.strings['Heading 3'] = 'Encapçalament 3';
+jsToolBar.strings['Unordered list'] = 'Llista sense ordre';
+jsToolBar.strings['Ordered list'] = 'Llista ordenada';
+jsToolBar.strings['Quote'] = 'Cometes';
+jsToolBar.strings['Unquote'] = 'Sense cometes';
+jsToolBar.strings['Preformatted text'] = 'Text formatat';
+jsToolBar.strings['Wiki link'] = 'Enllaça a una pàgina Wiki';
+jsToolBar.strings['Image'] = 'Imatge';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Tučné';
+jsToolBar.strings['Italic'] = 'Kurzíva';
+jsToolBar.strings['Underline'] = 'Podtržené';
+jsToolBar.strings['Deleted'] = 'Přeškrtnuté ';
+jsToolBar.strings['Code'] = 'Zobrazení kódu';
+jsToolBar.strings['Heading 1'] = 'Záhlaví 1';
+jsToolBar.strings['Heading 2'] = 'Záhlaví 2';
+jsToolBar.strings['Heading 3'] = 'Záhlaví 3';
+jsToolBar.strings['Unordered list'] = 'Seznam';
+jsToolBar.strings['Ordered list'] = 'Uspořádaný seznam';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Předformátovaný text';
+jsToolBar.strings['Wiki link'] = 'Vložit odkaz na Wiki stránku';
+jsToolBar.strings['Image'] = 'Vložit obrázek';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Fed';
+jsToolBar.strings['Italic'] = 'Kursiv';
+jsToolBar.strings['Underline'] = 'Understreget';
+jsToolBar.strings['Deleted'] = 'Slettet';
+jsToolBar.strings['Code'] = 'Inline-kode';
+jsToolBar.strings['Heading 1'] = 'Overskrift 1';
+jsToolBar.strings['Heading 2'] = 'Overskrift 2';
+jsToolBar.strings['Heading 3'] = 'Overskrift 3';
+jsToolBar.strings['Unordered list'] = 'Unummereret liste';
+jsToolBar.strings['Ordered list'] = 'Nummereret liste';
+jsToolBar.strings['Quote'] = 'Citér';
+jsToolBar.strings['Unquote'] = 'Fjern citér';
+jsToolBar.strings['Preformatted text'] = 'Præformateret tekst';
+jsToolBar.strings['Wiki link'] = 'Link til en wiki-side';
+jsToolBar.strings['Image'] = 'Billede';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Fett';
+jsToolBar.strings['Italic'] = 'Kursiv';
+jsToolBar.strings['Underline'] = 'Unterstrichen';
+jsToolBar.strings['Deleted'] = 'Durchgestrichen';
+jsToolBar.strings['Code'] = 'Quelltext';
+jsToolBar.strings['Heading 1'] = 'Überschrift 1. Ordnung';
+jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung';
+jsToolBar.strings['Heading 3'] = 'Überschrift 3. Ordnung';
+jsToolBar.strings['Unordered list'] = 'Aufzählungsliste';
+jsToolBar.strings['Ordered list'] = 'Nummerierte Liste';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Präformatierter Text';
+jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite';
+jsToolBar.strings['Image'] = 'Grafik';
--- /dev/null
+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';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Negrita';
+jsToolBar.strings['Italic'] = 'Itálica';
+jsToolBar.strings['Underline'] = 'Subrayado';
+jsToolBar.strings['Deleted'] = 'Tachado';
+jsToolBar.strings['Code'] = 'Código fuente';
+jsToolBar.strings['Heading 1'] = 'Encabezado 1';
+jsToolBar.strings['Heading 2'] = 'Encabezado 2';
+jsToolBar.strings['Heading 3'] = 'Encabezado 3';
+jsToolBar.strings['Unordered list'] = 'Lista sin ordenar';
+jsToolBar.strings['Ordered list'] = 'Lista ordenada';
+jsToolBar.strings['Quote'] = 'Citar';
+jsToolBar.strings['Unquote'] = 'Quitar cita';
+jsToolBar.strings['Preformatted text'] = 'Texto con formato';
+jsToolBar.strings['Wiki link'] = 'Enlace a página Wiki';
+jsToolBar.strings['Image'] = 'Imagen';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Lihavoitu';
+jsToolBar.strings['Italic'] = 'Kursivoitu';
+jsToolBar.strings['Underline'] = 'Alleviivattu';
+jsToolBar.strings['Deleted'] = 'Yliviivattu';
+jsToolBar.strings['Code'] = 'Koodi näkymä';
+jsToolBar.strings['Heading 1'] = 'Otsikko 1';
+jsToolBar.strings['Heading 2'] = 'Otsikko 2';
+jsToolBar.strings['Heading 3'] = 'Otsikko 3';
+jsToolBar.strings['Unordered list'] = 'Järjestämätön lista';
+jsToolBar.strings['Ordered list'] = 'Järjestetty lista';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Ennaltamuotoiltu teksti';
+jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle';
+jsToolBar.strings['Image'] = 'Kuva';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Gras';
+jsToolBar.strings['Italic'] = 'Italique';
+jsToolBar.strings['Underline'] = 'Souligné';
+jsToolBar.strings['Deleted'] = 'Rayé';
+jsToolBar.strings['Code'] = 'Code en ligne';
+jsToolBar.strings['Heading 1'] = 'Titre niveau 1';
+jsToolBar.strings['Heading 2'] = 'Titre niveau 2';
+jsToolBar.strings['Heading 3'] = 'Titre niveau 3';
+jsToolBar.strings['Unordered list'] = 'Liste à puces';
+jsToolBar.strings['Ordered list'] = 'Liste numérotée';
+jsToolBar.strings['Quote'] = 'Citer';
+jsToolBar.strings['Unquote'] = 'Supprimer citation';
+jsToolBar.strings['Preformatted text'] = 'Texte préformaté';
+jsToolBar.strings['Wiki link'] = 'Lien vers une page Wiki';
+jsToolBar.strings['Image'] = 'Image';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Negriña';
+jsToolBar.strings['Italic'] = 'Itálica';
+jsToolBar.strings['Underline'] = 'Suliñado';
+jsToolBar.strings['Deleted'] = 'Tachado';
+jsToolBar.strings['Code'] = 'Código fonte';
+jsToolBar.strings['Heading 1'] = 'Encabezado 1';
+jsToolBar.strings['Heading 2'] = 'Encabezado 2';
+jsToolBar.strings['Heading 3'] = 'Encabezado 3';
+jsToolBar.strings['Unordered list'] = 'Lista sen ordenar';
+jsToolBar.strings['Ordered list'] = 'Lista ordenada';
+jsToolBar.strings['Quote'] = 'Citar';
+jsToolBar.strings['Unquote'] = 'Quitar cita';
+jsToolBar.strings['Preformatted text'] = 'Texto con formato';
+jsToolBar.strings['Wiki link'] = 'Enlace a páxina Wiki';
+jsToolBar.strings['Image'] = 'Imaxe';
--- /dev/null
+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';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Félkövér';
+jsToolBar.strings['Italic'] = 'Dőlt';
+jsToolBar.strings['Underline'] = 'Aláhúzott';
+jsToolBar.strings['Deleted'] = 'Törölt';
+jsToolBar.strings['Code'] = 'Kód sorok';
+jsToolBar.strings['Heading 1'] = 'Fejléc 1';
+jsToolBar.strings['Heading 2'] = 'Fejléc 2';
+jsToolBar.strings['Heading 3'] = 'Fejléc 3';
+jsToolBar.strings['Unordered list'] = 'Felsorolás';
+jsToolBar.strings['Ordered list'] = 'Számozott lista';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Előreformázott szöveg';
+jsToolBar.strings['Wiki link'] = 'Link egy Wiki oldalra';
+jsToolBar.strings['Image'] = 'Kép';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Grassetto';
+jsToolBar.strings['Italic'] = 'Corsivo';
+jsToolBar.strings['Underline'] = 'Sottolineato';
+jsToolBar.strings['Deleted'] = 'Barrato';
+jsToolBar.strings['Code'] = 'Codice sorgente';
+jsToolBar.strings['Heading 1'] = 'Titolo 1';
+jsToolBar.strings['Heading 2'] = 'Titolo 2';
+jsToolBar.strings['Heading 3'] = 'Titolo 3';
+jsToolBar.strings['Unordered list'] = 'Elenco puntato';
+jsToolBar.strings['Ordered list'] = 'Numerazione';
+jsToolBar.strings['Quote'] = 'Aumenta rientro';
+jsToolBar.strings['Unquote'] = 'Riduci rientro';
+jsToolBar.strings['Preformatted text'] = 'Testo preformattato';
+jsToolBar.strings['Wiki link'] = 'Collegamento a pagina Wiki';
+jsToolBar.strings['Image'] = 'Immagine';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = '強調';
+jsToolBar.strings['Italic'] = '斜体';
+jsToolBar.strings['Underline'] = '下線';
+jsToolBar.strings['Deleted'] = '取り消し線';
+jsToolBar.strings['Code'] = 'コード';
+jsToolBar.strings['Heading 1'] = '見出し 1';
+jsToolBar.strings['Heading 2'] = '見出し 2';
+jsToolBar.strings['Heading 3'] = '見出し 3';
+jsToolBar.strings['Unordered list'] = '順不同リスト';
+jsToolBar.strings['Ordered list'] = '番号つきリスト';
+jsToolBar.strings['Quote'] = '引用';
+jsToolBar.strings['Unquote'] = '引用解除';
+jsToolBar.strings['Preformatted text'] = '整形済みテキスト';
+jsToolBar.strings['Wiki link'] = 'Wikiページへのリンク';
+jsToolBar.strings['Image'] = '画像';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = '굵게';
+jsToolBar.strings['Italic'] = '기울임';
+jsToolBar.strings['Underline'] = '밑줄';
+jsToolBar.strings['Deleted'] = '취소선';
+jsToolBar.strings['Code'] = '코드';
+jsToolBar.strings['Heading 1'] = '제목 1';
+jsToolBar.strings['Heading 2'] = '제목 2';
+jsToolBar.strings['Heading 3'] = '제목 3';
+jsToolBar.strings['Unordered list'] = '글머리 기호';
+jsToolBar.strings['Ordered list'] = '번호 매기기';
+jsToolBar.strings['Quote'] = '인용';
+jsToolBar.strings['Unquote'] = '인용 취소';
+jsToolBar.strings['Preformatted text'] = '있는 그대로 표현 (Preformatted text)';
+jsToolBar.strings['Wiki link'] = 'Wiki 페이지에 연결';
+jsToolBar.strings['Image'] = '그림';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Pastorinti';
+jsToolBar.strings['Italic'] = 'Italic';
+jsToolBar.strings['Underline'] = 'Pabraukti';
+jsToolBar.strings['Deleted'] = 'Užbraukti';
+jsToolBar.strings['Code'] = 'Kodas';
+jsToolBar.strings['Heading 1'] = 'Heading 1';
+jsToolBar.strings['Heading 2'] = 'Heading 2';
+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['Preformatted text'] = 'Preformatuotas tekstas';
+jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį';
+jsToolBar.strings['Image'] = 'Paveikslas';
--- /dev/null
+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'] = 'Подредена листа';
+jsToolBar.strings['Quote'] = 'Цитат';
+jsToolBar.strings['Unquote'] = 'Отстрани цитат';
+jsToolBar.strings['Preformatted text'] = 'Preformatted text';
+jsToolBar.strings['Wiki link'] = 'Линк до вики страна';
+jsToolBar.strings['Image'] = 'Слика';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Extra nadruk';
+jsToolBar.strings['Italic'] = 'Cursief';
+jsToolBar.strings['Underline'] = 'Onderstreept';
+jsToolBar.strings['Deleted'] = 'Verwijderd';
+jsToolBar.strings['Code'] = 'Computercode';
+jsToolBar.strings['Heading 1'] = 'Kop 1';
+jsToolBar.strings['Heading 2'] = 'Kop 2';
+jsToolBar.strings['Heading 3'] = 'Kop 3';
+jsToolBar.strings['Unordered list'] = 'Ongeordende lijst';
+jsToolBar.strings['Ordered list'] = 'Geordende lijst';
+jsToolBar.strings['Quote'] = 'Citaat';
+jsToolBar.strings['Unquote'] = 'Verwijder citaat';
+jsToolBar.strings['Preformatted text'] = 'Voor-geformateerde tekst';
+jsToolBar.strings['Wiki link'] = 'Link naar een Wiki pagina';
+jsToolBar.strings['Image'] = 'Afbeelding';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Fet';
+jsToolBar.strings['Italic'] = 'Kursiv';
+jsToolBar.strings['Underline'] = 'Understreking';
+jsToolBar.strings['Deleted'] = 'Slettet';
+jsToolBar.strings['Code'] = 'Kode';
+jsToolBar.strings['Heading 1'] = 'Overskrift 1';
+jsToolBar.strings['Heading 2'] = 'Overskrift 2';
+jsToolBar.strings['Heading 3'] = 'Overskrift 3';
+jsToolBar.strings['Unordered list'] = 'Punktliste';
+jsToolBar.strings['Ordered list'] = 'Nummerert liste';
+jsToolBar.strings['Quote'] = 'Sitat';
+jsToolBar.strings['Unquote'] = 'Avslutt sitat';
+jsToolBar.strings['Preformatted text'] = 'Preformatert tekst';
+jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side';
+jsToolBar.strings['Image'] = 'Bilde';
--- /dev/null
+// Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BF idea...
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Pogrubienie';
+jsToolBar.strings['Italic'] = 'Kursywa';
+jsToolBar.strings['Underline'] = 'Podkreślenie';
+jsToolBar.strings['Deleted'] = 'Usunięte';
+jsToolBar.strings['Code'] = 'Wstawka kodu';
+jsToolBar.strings['Heading 1'] = 'Nagłowek 1';
+jsToolBar.strings['Heading 2'] = 'Nagłówek 2';
+jsToolBar.strings['Heading 3'] = 'Nagłówek 3';
+jsToolBar.strings['Unordered list'] = 'Nieposortowana lista';
+jsToolBar.strings['Ordered list'] = 'Posortowana lista';
+jsToolBar.strings['Quote'] = 'Cytat';
+jsToolBar.strings['Unquote'] = 'Usuń cytat';
+jsToolBar.strings['Preformatted text'] = 'Sformatowany tekst';
+jsToolBar.strings['Wiki link'] = 'Odnośnik do strony Wiki';
+jsToolBar.strings['Image'] = 'Obraz';
--- /dev/null
+// Translated by: Alexandre da Silva <simpsomboy@gmail.com>
+
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Negrito';
+jsToolBar.strings['Italic'] = 'Itálico';
+jsToolBar.strings['Underline'] = 'Sublinhado';
+jsToolBar.strings['Deleted'] = 'Excluído';
+jsToolBar.strings['Code'] = 'Código Inline';
+jsToolBar.strings['Heading 1'] = 'Cabeçalho 1';
+jsToolBar.strings['Heading 2'] = 'Cabeçalho 2';
+jsToolBar.strings['Heading 3'] = 'Cabeçalho 3';
+jsToolBar.strings['Unordered list'] = 'Lista não ordenada';
+jsToolBar.strings['Ordered list'] = 'Lista ordenada';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado';
+jsToolBar.strings['Wiki link'] = 'Link para uma página Wiki';
+jsToolBar.strings['Image'] = 'Imagem';
--- /dev/null
+// Translated by: Pedro Araújo <phcrva19@hotmail.com>
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Negrito';
+jsToolBar.strings['Italic'] = 'Itálico';
+jsToolBar.strings['Underline'] = 'Sublinhado';
+jsToolBar.strings['Deleted'] = 'Apagado';
+jsToolBar.strings['Code'] = 'Código Inline';
+jsToolBar.strings['Heading 1'] = 'Cabeçalho 1';
+jsToolBar.strings['Heading 2'] = 'Cabeçalho 2';
+jsToolBar.strings['Heading 3'] = 'Cabeçalho 3';
+jsToolBar.strings['Unordered list'] = 'Lista não ordenada';
+jsToolBar.strings['Ordered list'] = 'Lista ordenada';
+jsToolBar.strings['Quote'] = 'Citação';
+jsToolBar.strings['Unquote'] = 'Remover citação';
+jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado';
+jsToolBar.strings['Wiki link'] = 'Link para uma página da Wiki';
+jsToolBar.strings['Image'] = 'Imagem';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Bold';
+jsToolBar.strings['Italic'] = 'Italic';
+jsToolBar.strings['Underline'] = 'Subliniat';
+jsToolBar.strings['Deleted'] = 'Șters';
+jsToolBar.strings['Code'] = 'Fragment de cod';
+jsToolBar.strings['Heading 1'] = 'Heading 1';
+jsToolBar.strings['Heading 2'] = 'Heading 2';
+jsToolBar.strings['Heading 3'] = 'Heading 3';
+jsToolBar.strings['Unordered list'] = 'Listă pe puncte';
+jsToolBar.strings['Ordered list'] = 'Listă ordonată';
+jsToolBar.strings['Quote'] = 'Citează';
+jsToolBar.strings['Unquote'] = 'Fără citat';
+jsToolBar.strings['Preformatted text'] = 'Text preformatat';
+jsToolBar.strings['Wiki link'] = 'Trimitere către o pagină wiki';
+jsToolBar.strings['Image'] = 'Imagine';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Жирный';
+jsToolBar.strings['Italic'] = 'Курсив';
+jsToolBar.strings['Underline'] = 'Подчеркнутый';
+jsToolBar.strings['Deleted'] = 'Зачеркнутый';
+jsToolBar.strings['Code'] = 'Вставка кода';
+jsToolBar.strings['Heading 1'] = 'Заголовок 1';
+jsToolBar.strings['Heading 2'] = 'Заголовок 2';
+jsToolBar.strings['Heading 3'] = 'Заголовок 3';
+jsToolBar.strings['Unordered list'] = 'Маркированный список';
+jsToolBar.strings['Ordered list'] = 'Нумерованный список';
+jsToolBar.strings['Quote'] = 'Цитата';
+jsToolBar.strings['Unquote'] = 'Удалить цитату';
+jsToolBar.strings['Preformatted text'] = 'Заранее форматированный текст';
+jsToolBar.strings['Wiki link'] = 'Ссылка на страницу в Wiki';
+jsToolBar.strings['Image'] = 'Вставка изображения';
--- /dev/null
+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'] = '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['Preformatted text'] = 'Predformátovaný text';
+jsToolBar.strings['Wiki link'] = 'Link na Wiki stránku';
+jsToolBar.strings['Image'] = 'Obrázok';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Krepko';
+jsToolBar.strings['Italic'] = 'Poševno';
+jsToolBar.strings['Underline'] = 'Podčrtano';
+jsToolBar.strings['Deleted'] = 'Izbrisano';
+jsToolBar.strings['Code'] = 'Koda med vrsticami';
+jsToolBar.strings['Heading 1'] = 'Naslov 1';
+jsToolBar.strings['Heading 2'] = 'Naslov 2';
+jsToolBar.strings['Heading 3'] = 'Naslov 3';
+jsToolBar.strings['Unordered list'] = 'Neurejen seznam';
+jsToolBar.strings['Ordered list'] = 'Urejen seznam';
+jsToolBar.strings['Quote'] = 'Citat';
+jsToolBar.strings['Unquote'] = 'Odstrani citat';
+jsToolBar.strings['Preformatted text'] = 'Predoblikovano besedilo';
+jsToolBar.strings['Wiki link'] = 'Povezava na Wiki stran';
+jsToolBar.strings['Image'] = 'Slika';
--- /dev/null
+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';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Fet';
+jsToolBar.strings['Italic'] = 'Kursiv';
+jsToolBar.strings['Underline'] = 'Understruken';
+jsToolBar.strings['Deleted'] = 'Genomstruken';
+jsToolBar.strings['Code'] = 'Kod';
+jsToolBar.strings['Heading 1'] = 'Rubrik 1';
+jsToolBar.strings['Heading 2'] = 'Rubrik 2';
+jsToolBar.strings['Heading 3'] = 'Rubrik 3';
+jsToolBar.strings['Unordered list'] = 'Osorterad lista';
+jsToolBar.strings['Ordered list'] = 'Sorterad lista';
+jsToolBar.strings['Quote'] = 'Citat';
+jsToolBar.strings['Unquote'] = 'Ta bort citat';
+jsToolBar.strings['Preformatted text'] = 'Förformaterad text';
+jsToolBar.strings['Wiki link'] = 'Länk till en wikisida';
+jsToolBar.strings['Image'] = 'Bild';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'หนา';
+jsToolBar.strings['Italic'] = 'เอียง';
+jsToolBar.strings['Underline'] = 'ขีดเส้นใต้';
+jsToolBar.strings['Deleted'] = 'ขีดฆ่า';
+jsToolBar.strings['Code'] = 'โค๊ดโปรแกรม';
+jsToolBar.strings['Heading 1'] = 'หัวข้อ 1';
+jsToolBar.strings['Heading 2'] = 'หัวข้อ 2';
+jsToolBar.strings['Heading 3'] = 'หัวข้อ 3';
+jsToolBar.strings['Unordered list'] = 'รายการ';
+jsToolBar.strings['Ordered list'] = 'ลำดับเลข';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'รูปแบบข้อความคงที่';
+jsToolBar.strings['Wiki link'] = 'เชื่อมโยงไปหน้า Wiki อื่น';
+jsToolBar.strings['Image'] = 'รูปภาพ';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Kalın';
+jsToolBar.strings['Italic'] = 'İtalik';
+jsToolBar.strings['Underline'] = 'Altı çizgili';
+jsToolBar.strings['Deleted'] = 'Silinmiş';
+jsToolBar.strings['Code'] = 'Satır içi kod';
+jsToolBar.strings['Heading 1'] = 'Başlık 1';
+jsToolBar.strings['Heading 2'] = 'Başlık 2';
+jsToolBar.strings['Heading 3'] = 'Başlık 3';
+jsToolBar.strings['Unordered list'] = 'Sırasız liste';
+jsToolBar.strings['Ordered list'] = 'Sıralı liste';
+jsToolBar.strings['Preformatted text'] = 'Preformatted text';
+jsToolBar.strings['Wiki link'] = 'Wiki sayfasına bağlantı';
+jsToolBar.strings['Image'] = 'Resim';
--- /dev/null
+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';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Đậm';
+jsToolBar.strings['Italic'] = 'Nghiêng';
+jsToolBar.strings['Underline'] = 'Gạch chân';
+jsToolBar.strings['Deleted'] = 'Xóa';
+jsToolBar.strings['Code'] = 'Mã chung dòng';
+jsToolBar.strings['Heading 1'] = 'Tiêu đề 1';
+jsToolBar.strings['Heading 2'] = 'Tiêu đề 2';
+jsToolBar.strings['Heading 3'] = 'Tiêu đề 3';
+jsToolBar.strings['Unordered list'] = 'Danh sách không thứ tự';
+jsToolBar.strings['Ordered list'] = 'Danh sách có thứ tự';
+jsToolBar.strings['Quote'] = 'Trích dẫn';
+jsToolBar.strings['Unquote'] = 'Bỏ trích dẫn';
+jsToolBar.strings['Preformatted text'] = 'Mã nguồn';
+jsToolBar.strings['Wiki link'] = 'Liên kết đến trang wiki';
+jsToolBar.strings['Image'] = 'Ảnh';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = '粗體';
+jsToolBar.strings['Italic'] = '斜體';
+jsToolBar.strings['Underline'] = '底線';
+jsToolBar.strings['Deleted'] = '刪除線';
+jsToolBar.strings['Code'] = '程式碼';
+jsToolBar.strings['Heading 1'] = '標題 1';
+jsToolBar.strings['Heading 2'] = '標題 2';
+jsToolBar.strings['Heading 3'] = '標題 3';
+jsToolBar.strings['Unordered list'] = '項目清單';
+jsToolBar.strings['Ordered list'] = '編號清單';
+jsToolBar.strings['Quote'] = '引文';
+jsToolBar.strings['Unquote'] = '取消引文';
+jsToolBar.strings['Preformatted text'] = '已格式文字';
+jsToolBar.strings['Wiki link'] = '連結至 Wiki 頁面';
+jsToolBar.strings['Image'] = '圖片';
--- /dev/null
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = '粗体';
+jsToolBar.strings['Italic'] = '斜体';
+jsToolBar.strings['Underline'] = '下划线';
+jsToolBar.strings['Deleted'] = '删除线';
+jsToolBar.strings['Code'] = '程序代码';
+jsToolBar.strings['Heading 1'] = '标题 1';
+jsToolBar.strings['Heading 2'] = '标题 2';
+jsToolBar.strings['Heading 3'] = '标题 3';
+jsToolBar.strings['Unordered list'] = '无序列表';
+jsToolBar.strings['Ordered list'] = '排序列表';
+jsToolBar.strings['Quote'] = '引用';
+jsToolBar.strings['Unquote'] = '删除引用';
+jsToolBar.strings['Preformatted text'] = '格式化文本';
+jsToolBar.strings['Wiki link'] = '连接到 Wiki 页面';
+jsToolBar.strings['Image'] = '图片';
--- /dev/null
+/* ***** BEGIN LICENSE BLOCK *****
+ * This file is part of DotClear.
+ * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
+ * rights reserved.
+ *
+ * DotClear is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * DotClear is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DotClear; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * ***** END LICENSE BLOCK *****
+*/
+
+/* Modified by JP LANG for textile formatting */
+
+// strong
+jsToolBar.prototype.elements.strong = {
+ type: 'button',
+ title: 'Strong',
+ fn: {
+ wiki: function() { this.singleTag('*') }
+ }
+}
+
+// em
+jsToolBar.prototype.elements.em = {
+ type: 'button',
+ title: 'Italic',
+ fn: {
+ wiki: function() { this.singleTag("_") }
+ }
+}
+
+// ins
+jsToolBar.prototype.elements.ins = {
+ type: 'button',
+ title: 'Underline',
+ fn: {
+ wiki: function() { this.singleTag('+') }
+ }
+}
+
+// del
+jsToolBar.prototype.elements.del = {
+ type: 'button',
+ title: 'Deleted',
+ fn: {
+ wiki: function() { this.singleTag('-') }
+ }
+}
+
+// code
+jsToolBar.prototype.elements.code = {
+ type: 'button',
+ title: 'Code',
+ fn: {
+ wiki: function() { this.singleTag('@') }
+ }
+}
+
+// spacer
+jsToolBar.prototype.elements.space1 = {type: 'space'}
+
+// headings
+jsToolBar.prototype.elements.h1 = {
+ type: 'button',
+ title: 'Heading 1',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('h1. ', '',function(str) {
+ str = str.replace(/^h\d+\.\s+/, '')
+ return str;
+ });
+ }
+ }
+}
+jsToolBar.prototype.elements.h2 = {
+ type: 'button',
+ title: 'Heading 2',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('h2. ', '',function(str) {
+ str = str.replace(/^h\d+\.\s+/, '')
+ return str;
+ });
+ }
+ }
+}
+jsToolBar.prototype.elements.h3 = {
+ type: 'button',
+ title: 'Heading 3',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('h3. ', '',function(str) {
+ str = str.replace(/^h\d+\.\s+/, '')
+ return str;
+ });
+ }
+ }
+}
+
+// spacer
+jsToolBar.prototype.elements.space2 = {type: 'space'}
+
+// ul
+jsToolBar.prototype.elements.ul = {
+ type: 'button',
+ title: 'Unordered list',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
+ });
+ }
+ }
+}
+
+// ol
+jsToolBar.prototype.elements.ol = {
+ type: 'button',
+ title: 'Ordered list',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^)[*-]?\s*/g,"$1# ");
+ });
+ }
+ }
+}
+
+// spacer
+jsToolBar.prototype.elements.space3 = {type: 'space'}
+
+// bq
+jsToolBar.prototype.elements.bq = {
+ type: 'button',
+ title: 'Quote',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
+ });
+ }
+ }
+}
+
+// unbq
+jsToolBar.prototype.elements.unbq = {
+ type: 'button',
+ title: 'Unquote',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
+ });
+ }
+ }
+}
+
+// pre
+jsToolBar.prototype.elements.pre = {
+ type: 'button',
+ title: 'Preformatted text',
+ fn: {
+ wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') }
+ }
+}
+
+// spacer
+jsToolBar.prototype.elements.space4 = {type: 'space'}
+
+// wiki page
+jsToolBar.prototype.elements.link = {
+ type: 'button',
+ title: 'Wiki link',
+ fn: {
+ wiki: function() { this.encloseSelection("[[", "]]") }
+ }
+}
+// image
+jsToolBar.prototype.elements.img = {
+ type: 'button',
+ title: 'Image',
+ fn: {
+ wiki: function() { this.encloseSelection("!", "!") }
+ }
+}
--- /dev/null
+/* Prototype JavaScript framework, version 1.6.0.3
+ * (c) 2005-2008 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.6.0.3',
+
+ Browser: {
+ IE: !!(window.attachEvent &&
+ navigator.userAgent.indexOf('Opera') === -1),
+ Opera: navigator.userAgent.indexOf('Opera') > -1,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 &&
+ navigator.userAgent.indexOf('KHTML') === -1,
+ MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+ },
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+ SelectorsAPI: !!document.querySelector,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ document.createElement('div')['__proto__'] &&
+ document.createElement('div')['__proto__'] !==
+ document.createElement('form')['__proto__']
+ },
+
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+ K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+ create: function() {
+ var parent = null, properties = $A(arguments);
+ if (Object.isFunction(properties[0]))
+ parent = properties.shift();
+
+ function klass() {
+ this.initialize.apply(this, arguments);
+ }
+
+ Object.extend(klass, Class.Methods);
+ klass.superclass = parent;
+ klass.subclasses = [];
+
+ if (parent) {
+ var subclass = function() { };
+ subclass.prototype = parent.prototype;
+ klass.prototype = new subclass;
+ parent.subclasses.push(klass);
+ }
+
+ for (var i = 0; i < properties.length; i++)
+ klass.addMethods(properties[i]);
+
+ if (!klass.prototype.initialize)
+ klass.prototype.initialize = Prototype.emptyFunction;
+
+ klass.prototype.constructor = klass;
+
+ return klass;
+ }
+};
+
+Class.Methods = {
+ addMethods: function(source) {
+ var ancestor = this.superclass && this.superclass.prototype;
+ var properties = Object.keys(source);
+
+ if (!Object.keys({ toString: true }).length)
+ properties.push("toString", "valueOf");
+
+ for (var i = 0, length = properties.length; i < length; i++) {
+ var property = properties[i], value = source[property];
+ if (ancestor && Object.isFunction(value) &&
+ value.argumentNames().first() == "$super") {
+ var method = value;
+ value = (function(m) {
+ return function() { return ancestor[m].apply(this, arguments) };
+ })(property).wrap(method);
+
+ value.valueOf = method.valueOf.bind(method);
+ value.toString = method.toString.bind(method);
+ }
+ this.prototype[property] = value;
+ }
+
+ return this;
+ }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+};
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (Object.isUndefined(object)) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : String(object);
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ toJSON: function(object) {
+ var type = typeof object;
+ switch (type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (Object.isElement(object)) return;
+
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (!Object.isUndefined(value))
+ results.push(property.toJSON() + ': ' + value);
+ }
+
+ return '{' + results.join(', ') + '}';
+ },
+
+ toQueryString: function(object) {
+ return $H(object).toQueryString();
+ },
+
+ toHTML: function(object) {
+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({ }, object);
+ },
+
+ isElement: function(object) {
+ return !!(object && object.nodeType == 1);
+ },
+
+ isArray: function(object) {
+ return object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object;
+ },
+
+ isHash: function(object) {
+ return object instanceof Hash;
+ },
+
+ isFunction: function(object) {
+ return typeof object == "function";
+ },
+
+ isString: function(object) {
+ return typeof object == "string";
+ },
+
+ isNumber: function(object) {
+ return typeof object == "number";
+ },
+
+ isUndefined: function(object) {
+ return typeof object == "undefined";
+ }
+});
+
+Object.extend(Function.prototype, {
+ argumentNames: function() {
+ var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
+ .replace(/\s+/g, '').split(',');
+ return names.length == 1 && !names[0] ? [] : names;
+ },
+
+ bind: function() {
+ if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+ },
+
+ bindAsEventListener: function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
+ },
+
+ curry: function() {
+ if (!arguments.length) return this;
+ var __method = this, args = $A(arguments);
+ return function() {
+ return __method.apply(this, args.concat($A(arguments)));
+ }
+ },
+
+ delay: function() {
+ var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+ return window.setTimeout(function() {
+ return __method.apply(__method, args);
+ }, timeout);
+ },
+
+ defer: function() {
+ var args = [0.01].concat($A(arguments));
+ return this.delay.apply(this, args);
+ },
+
+ wrap: function(wrapper) {
+ var __method = this;
+ return function() {
+ return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+ }
+ },
+
+ methodize: function() {
+ if (this._methodized) return this._methodized;
+ var __method = this;
+ return this._methodized = function() {
+ return __method.apply(null, [this].concat($A(arguments)));
+ };
+ }
+});
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) { }
+ }
+
+ return returnValue;
+ }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ execute: function() {
+ this.callback(this);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.execute();
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+});
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = Object.isUndefined(count) ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return String(this);
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : String(this);
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = new Element('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return { };
+
+ return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ times: function(count) {
+ return count < 1 ? '' : new Array(count + 1).join(this);
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this;
+ if (str.blank()) return false;
+ str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ },
+
+ interpolate: function(object, pattern) {
+ return new Template(this, pattern).evaluate(object);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
+ },
+ unescapeHTML: function() {
+ return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (Object.isFunction(replacement)) return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);
+
+var Template = Class.create({
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ if (Object.isFunction(object.toTemplateReplacements))
+ object = object.toTemplateReplacements();
+
+ return this.template.gsub(this.pattern, function(match) {
+ if (object == null) return '';
+
+ var before = match[1] || '';
+ if (before == '\\') return match[2];
+
+ var ctx = object, expr = match[3];
+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + String.interpret(ctx);
+ });
+ }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+ each: function(iterator, context) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ iterator.call(context, value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator, context) {
+ var index = -number, slices = [], array = this.toArray();
+ if (number < 1) return array;
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.collect(iterator, context);
+ },
+
+ all: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!iterator.call(context, value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!iterator.call(context, value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator, context) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(filter, iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+
+ if (Object.isString(filter))
+ filter = new RegExp(filter);
+
+ this.each(function(value, index) {
+ if (filter.match(value))
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ },
+
+ include: function(object) {
+ if (Object.isFunction(this.indexOf))
+ if (this.indexOf(object) != -1) return true;
+
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator, context) {
+ this.each(function(value, index) {
+ memo = iterator.call(context, memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ (iterator.call(context, value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator, context) {
+ return this.map(function(value, index) {
+ return {
+ value: value,
+ criteria: iterator.call(context, value, index)
+ };
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (Object.isFunction(args.last()))
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+};
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ filter: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray,
+ every: Enumerable.all,
+ some: Enumerable.any
+});
+function $A(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) return iterable.toArray();
+ var length = iterable.length || 0, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+}
+
+if (Prototype.Browser.WebKit) {
+ $A = function(iterable) {
+ if (!iterable) return [];
+ // In Safari, only use the `toArray` method if it's not a NodeList.
+ // A NodeList is a function, has an function `item` property, and a numeric
+ // `length` property. Adapted from Google Doctype.
+ if (!(typeof iterable === 'function' && typeof iterable.length ===
+ 'number' && typeof iterable.item === 'function') && iterable.toArray)
+ return iterable.toArray();
+ var length = iterable.length || 0, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+ };
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(Object.isArray(value) ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ intersect: function(array) {
+ return this.uniq().findAll(function(item) {
+ return array.detect(function(value) { return item === value });
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (!Object.isUndefined(value)) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
+ }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+ Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+ i || (i = 0);
+ var length = this.length;
+ if (i < 0) i = length + i;
+ for (; i < length; i++)
+ if (this[i] === item) return i;
+ return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+ var n = this.slice(0, i).reverse().indexOf(item);
+ return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+ if (!Object.isString(string)) return [];
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+ Array.prototype.concat = function() {
+ var array = [];
+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ if (Object.isArray(arguments[i])) {
+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ };
+}
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator, context) {
+ $R(0, this, true).each(iterator, context);
+ return this;
+ },
+
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ },
+
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
+ }
+});
+
+$w('abs round ceil floor').each(function(method){
+ Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+ return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+
+ function toQueryPair(key, value) {
+ if (Object.isUndefined(value)) return key;
+ return key + '=' + encodeURIComponent(String.interpret(value));
+ }
+
+ return {
+ initialize: function(object) {
+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+ },
+
+ _each: function(iterator) {
+ for (var key in this._object) {
+ var value = this._object[key], pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ set: function(key, value) {
+ return this._object[key] = value;
+ },
+
+ get: function(key) {
+ // simulating poorly supported hasOwnProperty
+ if (this._object[key] !== Object.prototype[key])
+ return this._object[key];
+ },
+
+ unset: function(key) {
+ var value = this._object[key];
+ delete this._object[key];
+ return value;
+ },
+
+ toObject: function() {
+ return Object.clone(this._object);
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ index: function(value) {
+ var match = this.detect(function(pair) {
+ return pair.value === value;
+ });
+ return match && match.key;
+ },
+
+ merge: function(object) {
+ return this.clone().update(object);
+ },
+
+ update: function(object) {
+ return new Hash(object).inject(this, function(result, pair) {
+ result.set(pair.key, pair.value);
+ return result;
+ });
+ },
+
+ toQueryString: function() {
+ return this.inject([], function(results, pair) {
+ var key = encodeURIComponent(pair.key), values = pair.value;
+
+ if (values && typeof values == 'object') {
+ if (Object.isArray(values))
+ return results.concat(values.map(toQueryPair.curry(key)));
+ } else results.push(toQueryPair(key, values));
+ return results;
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Object.toJSON(this.toObject());
+ },
+
+ clone: function() {
+ return new Hash(this);
+ }
+ }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+};
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (Object.isFunction(responder[callback])) {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) { }
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() { Ajax.activeRequestCount++ },
+ onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+ initialize: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: '',
+ evalJSON: true,
+ evalJS: true
+ };
+ Object.extend(this.options, options || { });
+
+ this.options.method = this.options.method.toLowerCase();
+
+ if (Object.isString(this.options.parameters))
+ this.options.parameters = this.options.parameters.toQueryParams();
+ else if (Object.isHash(this.options.parameters))
+ this.options.parameters = this.options.parameters.toObject();
+ }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+ _complete: false,
+
+ initialize: function($super, url, options) {
+ $super(options);
+ this.transport = Ajax.getTransport();
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Object.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ var response = new Ajax.Response(this);
+ if (this.options.onCreate) this.options.onCreate(response);
+ Ajax.Responders.dispatch('onCreate', this, response);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (Object.isFunction(extras.push))
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ var status = this.getStatus();
+ return !status || (status >= 200 && status < 300);
+ },
+
+ getStatus: function() {
+ try {
+ return this.transport.status || 0;
+ } catch (e) { return 0 }
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + response.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = response.getHeader('Content-type');
+ if (this.options.evalJS == 'force'
+ || (this.options.evalJS && this.isSameOrigin() && contentType
+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ isSameOrigin: function() {
+ var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+ return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+ protocol: location.protocol,
+ domain: document.domain,
+ port: location.port ? ':' + location.port : ''
+ }));
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name) || null;
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+ initialize: function(request){
+ this.request = request;
+ var transport = this.transport = request.transport,
+ readyState = this.readyState = transport.readyState;
+
+ if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+ this.status = this.getStatus();
+ this.statusText = this.getStatusText();
+ this.responseText = String.interpret(transport.responseText);
+ this.headerJSON = this._getHeaderJSON();
+ }
+
+ if(readyState == 4) {
+ var xml = transport.responseXML;
+ this.responseXML = Object.isUndefined(xml) ? null : xml;
+ this.responseJSON = this._getResponseJSON();
+ }
+ },
+
+ status: 0,
+ statusText: '',
+
+ getStatus: Ajax.Request.prototype.getStatus,
+
+ getStatusText: function() {
+ try {
+ return this.transport.statusText || '';
+ } catch (e) { return '' }
+ },
+
+ getHeader: Ajax.Request.prototype.getHeader,
+
+ getAllHeaders: function() {
+ try {
+ return this.getAllResponseHeaders();
+ } catch (e) { return null }
+ },
+
+ getResponseHeader: function(name) {
+ return this.transport.getResponseHeader(name);
+ },
+
+ getAllResponseHeaders: function() {
+ return this.transport.getAllResponseHeaders();
+ },
+
+ _getHeaderJSON: function() {
+ var json = this.getHeader('X-JSON');
+ if (!json) return null;
+ json = decodeURIComponent(escape(json));
+ try {
+ return json.evalJSON(this.request.options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ },
+
+ _getResponseJSON: function() {
+ var options = this.request.options;
+ if (!options.evalJSON || (options.evalJSON != 'force' &&
+ !(this.getHeader('Content-type') || '').include('application/json')) ||
+ this.responseText.blank())
+ return null;
+ try {
+ return this.responseText.evalJSON(options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+ initialize: function($super, container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ };
+
+ options = Object.clone(options);
+ var onComplete = options.onComplete;
+ options.onComplete = (function(response, json) {
+ this.updateContent(response.responseText);
+ if (Object.isFunction(onComplete)) onComplete(response, json);
+ }).bind(this);
+
+ $super(url, options);
+ },
+
+ updateContent: function(responseText) {
+ var receiver = this.container[this.success() ? 'success' : 'failure'],
+ options = this.options;
+
+ if (!options.evalScripts) responseText = responseText.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (options.insertion) {
+ if (Object.isString(options.insertion)) {
+ var insertion = { }; insertion[options.insertion] = responseText;
+ receiver.insert(insertion);
+ }
+ else options.insertion(receiver, responseText);
+ }
+ else receiver.update(responseText);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+ initialize: function($super, container, url, options) {
+ $super(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = { };
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(response) {
+ if (this.options.decay) {
+ this.decay = (response.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = response.responseText;
+ }
+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (Object.isString(element))
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(Element.extend(query.snapshotItem(i)));
+ return results;
+ };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+ // DOM level 2 ECMAScript Language Binding
+ Object.extend(Node, {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ });
+}
+
+(function() {
+ var element = this.Element;
+ this.Element = function(tagName, attributes) {
+ attributes = attributes || { };
+ tagName = tagName.toLowerCase();
+ var cache = Element.cache;
+ if (Prototype.Browser.IE && attributes.name) {
+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
+ delete attributes.name;
+ return Element.writeAttribute(document.createElement(tagName), attributes);
+ }
+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+ };
+ Object.extend(this.Element, element || { });
+ if (element) this.Element.prototype = element.prototype;
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ element = $(element);
+ element.style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ element = $(element);
+ element.style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+ content = Object.toHTML(content);
+ element.innerHTML = content.stripScripts();
+ content.evalScripts.bind(content).defer();
+ return element;
+ },
+
+ replace: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ else if (!Object.isElement(content)) {
+ content = Object.toHTML(content);
+ var range = element.ownerDocument.createRange();
+ range.selectNode(element);
+ content.evalScripts.bind(content).defer();
+ content = range.createContextualFragment(content.stripScripts());
+ }
+ element.parentNode.replaceChild(content, element);
+ return element;
+ },
+
+ insert: function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = {bottom:insertions};
+
+ var content, insert, tagName, childNodes;
+
+ for (var position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ insert = Element._insertionTranslations[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+ if (position == 'top' || position == 'after') childNodes.reverse();
+ childNodes.each(insert.curry(element));
+
+ content.evalScripts.bind(content).defer();
+ }
+
+ return element;
+ },
+
+ wrap: function(element, wrapper, attributes) {
+ element = $(element);
+ if (Object.isElement(wrapper))
+ $(wrapper).writeAttribute(attributes || { });
+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+ else wrapper = new Element('div', wrapper);
+ if (element.parentNode)
+ element.parentNode.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ return wrapper;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $(element).select("*");
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (Object.isString(selector))
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return Object.isNumber(expression) ? ancestors[expression] :
+ Selector.findElement(ancestors, expression, index);
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ return Object.isNumber(expression) ? element.descendants()[expression] :
+ Element.select(element, expression)[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return Object.isNumber(expression) ? previousSiblings[expression] :
+ Selector.findElement(previousSiblings, expression, index);
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return Object.isNumber(expression) ? nextSiblings[expression] :
+ Selector.findElement(nextSiblings, expression, index);
+ },
+
+ select: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ adjacent: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element.parentNode, args).without(element);
+ },
+
+ identify: function(element) {
+ element = $(element);
+ var id = element.readAttribute('id'), self = arguments.callee;
+ if (id) return id;
+ do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+ element.writeAttribute('id', id);
+ return id;
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ var t = Element._attributeTranslations.read;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ if (name.include(':')) {
+ return (!element.attributes || !element.attributes[name]) ? null :
+ element.attributes[name].value;
+ }
+ }
+ return element.getAttribute(name);
+ },
+
+ writeAttribute: function(element, name, value) {
+ element = $(element);
+ var attributes = { }, t = Element._attributeTranslations.write;
+
+ if (typeof name == 'object') attributes = name;
+ else attributes[name] = Object.isUndefined(value) ? true : value;
+
+ for (var attr in attributes) {
+ name = t.names[attr] || attr;
+ value = attributes[attr];
+ if (t.values[attr]) name = t.values[attr](element, value);
+ if (value === false || value === null)
+ element.removeAttribute(name);
+ else if (value === true)
+ element.setAttribute(name, name);
+ else element.setAttribute(name, value);
+ }
+ return element;
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ return (elementClassName.length > 0 && (elementClassName == className ||
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ if (!element.hasClassName(className))
+ element.className += (element.className ? ' ' : '') + className;
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ element.className = element.className.replace(
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return element[element.hasClassName(className) ?
+ 'removeClassName' : 'addClassName'](className);
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+
+ if (element.compareDocumentPosition)
+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+ if (ancestor.contains)
+ return ancestor.contains(element) && ancestor !== element;
+
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = element.cumulativeOffset();
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value || value == 'auto') {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles) {
+ element = $(element);
+ var elementStyle = element.style, match;
+ if (Object.isString(styles)) {
+ element.style.cssText += ';' + styles;
+ return styles.include('opacity') ?
+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+ }
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property]);
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+ property] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = element.getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (Prototype.Browser.Opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+ if (element._overflow !== 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (element.tagName.toUpperCase() == 'BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p !== 'static') break;
+ }
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'absolute') return element;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ var offsets = element.positionedOffset();
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ return element;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'relative') return element;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ return element;
+ },
+
+ cumulativeScrollOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ getOffsetParent: function(element) {
+ if (element.offsetParent) return $(element.offsetParent);
+ if (element == document.body) return $(element);
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return $(element);
+
+ return $(document.body);
+ },
+
+ viewportOffset: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent == document.body &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ clonePosition: function(element, source) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || { });
+
+ // find page position of source
+ source = $(source);
+ var p = source.viewportOffset();
+
+ // find coordinate system to use
+ element = $(element);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(element, 'position') == 'absolute') {
+ parent = element.getOffsetParent();
+ delta = parent.viewportOffset();
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+ return element;
+ }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+ getElementsBySelector: Element.Methods.select,
+ childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+ write: {
+ names: {
+ className: 'class',
+ htmlFor: 'for'
+ },
+ values: { }
+ }
+};
+
+if (Prototype.Browser.Opera) {
+ Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+ function(proceed, element, style) {
+ switch (style) {
+ case 'left': case 'top': case 'right': case 'bottom':
+ if (proceed(element, 'position') === 'static') return null;
+ case 'height': case 'width':
+ // returns '0px' for hidden elements; we want it to return null
+ if (!Element.visible(element)) return null;
+
+ // returns the border-box dimensions rather than the content-box
+ // dimensions, so we subtract padding and borders from the value
+ var dim = parseInt(proceed(element, style), 10);
+
+ if (dim !== element['offset' + style.capitalize()])
+ return dim + 'px';
+
+ var properties;
+ if (style === 'height') {
+ properties = ['border-top-width', 'padding-top',
+ 'padding-bottom', 'border-bottom-width'];
+ }
+ else {
+ properties = ['border-left-width', 'padding-left',
+ 'padding-right', 'border-right-width'];
+ }
+ return properties.inject(dim, function(memo, property) {
+ var val = proceed(element, property);
+ return val === null ? memo : memo - parseInt(val, 10);
+ }) + 'px';
+ default: return proceed(element, style);
+ }
+ }
+ );
+
+ Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+ function(proceed, element, attribute) {
+ if (attribute === 'title') return element.title;
+ return proceed(element, attribute);
+ }
+ );
+}
+
+else if (Prototype.Browser.IE) {
+ // IE doesn't report offsets correctly for static elements, so we change them
+ // to "relative" to get the values, then change them back.
+ Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+ function(proceed, element) {
+ element = $(element);
+ // IE throws an error if element is not in document
+ try { element.offsetParent }
+ catch(e) { return $(document.body) }
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+
+ $w('positionedOffset viewportOffset').each(function(method) {
+ Element.Methods[method] = Element.Methods[method].wrap(
+ function(proceed, element) {
+ element = $(element);
+ try { element.offsetParent }
+ catch(e) { return Element._returnOffset(0,0) }
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ // Trigger hasLayout on the offset parent so that IE6 reports
+ // accurate offsetTop and offsetLeft values for position: fixed.
+ var offsetParent = element.getOffsetParent();
+ if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+ offsetParent.setStyle({ zoom: 1 });
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+ });
+
+ Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
+ function(proceed, element) {
+ try { element.offsetParent }
+ catch(e) { return Element._returnOffset(0,0) }
+ return proceed(element);
+ }
+ );
+
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset' + style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ function stripAlpha(filter){
+ return filter.replace(/alpha\([^\)]*\)/gi,'');
+ }
+ element = $(element);
+ var currentStyle = element.currentStyle;
+ if ((currentStyle && !currentStyle.hasLayout) ||
+ (!currentStyle && element.style.zoom == 'normal'))
+ element.style.zoom = 1;
+
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ (filter = stripAlpha(filter)) ?
+ style.filter = filter : style.removeAttribute('filter');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = stripAlpha(filter) +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ Element._attributeTranslations = {
+ read: {
+ names: {
+ 'class': 'className',
+ 'for': 'htmlFor'
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _getAttrNode: function(element, attribute) {
+ var node = element.getAttributeNode(attribute);
+ return node ? node.value : "";
+ },
+ _getEv: function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ return attribute ? attribute.toString().slice(23, -2) : null;
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ return element.title;
+ }
+ }
+ }
+ };
+
+ Element._attributeTranslations.write = {
+ names: Object.extend({
+ cellpadding: 'cellPadding',
+ cellspacing: 'cellSpacing'
+ }, Element._attributeTranslations.read.names),
+ values: {
+ checked: function(element, value) {
+ element.checked = !!value;
+ },
+
+ style: function(element, value) {
+ element.style.cssText = value ? value : '';
+ }
+ }
+ };
+
+ Element._attributeTranslations.has = {};
+
+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+ 'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+ });
+
+ (function(v) {
+ Object.extend(v, {
+ href: v._getAttr,
+ src: v._getAttr,
+ type: v._getAttr,
+ action: v._getAttrNode,
+ disabled: v._flag,
+ checked: v._flag,
+ readonly: v._flag,
+ multiple: v._flag,
+ onload: v._getEv,
+ onunload: v._getEv,
+ onclick: v._getEv,
+ ondblclick: v._getEv,
+ onmousedown: v._getEv,
+ onmouseup: v._getEv,
+ onmouseover: v._getEv,
+ onmousemove: v._getEv,
+ onmouseout: v._getEv,
+ onfocus: v._getEv,
+ onblur: v._getEv,
+ onkeypress: v._getEv,
+ onkeydown: v._getEv,
+ onkeyup: v._getEv,
+ onsubmit: v._getEv,
+ onreset: v._getEv,
+ onselect: v._getEv,
+ onchange: v._getEv
+ });
+ })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+else if (Prototype.Browser.WebKit) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+
+ if (value == 1)
+ if(element.tagName.toUpperCase() == 'IMG' && element.width) {
+ element.width++; element.width--;
+ } else try {
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch (e) { }
+
+ return element;
+ };
+
+ // Safari returns margins on body which is incorrect if the child is absolutely
+ // positioned. For performance reasons, redefine Element#cumulativeOffset for
+ // KHTML/WebKit only.
+ Element.Methods.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return Element._returnOffset(valueL, valueT);
+ };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+ // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+ Element.Methods.update = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+
+ content = Object.toHTML(content);
+ var tagName = element.tagName.toUpperCase();
+
+ if (tagName in Element._insertionTranslations.tags) {
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+ .each(function(node) { element.appendChild(node) });
+ }
+ else element.innerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+if ('outerHTML' in document.createElement('div')) {
+ Element.Methods.replace = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ element.parentNode.replaceChild(content, element);
+ return element;
+ }
+
+ content = Object.toHTML(content);
+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+ if (Element._insertionTranslations.tags[tagName]) {
+ var nextSibling = element.next();
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ parent.removeChild(element);
+ if (nextSibling)
+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+ else
+ fragments.each(function(node) { parent.appendChild(node) });
+ }
+ else element.outerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+Element._returnOffset = function(l, t) {
+ var result = [l, t];
+ result.left = l;
+ result.top = t;
+ return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+ var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+ if (t) {
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ } else div.innerHTML = html;
+ return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+ before: function(element, node) {
+ element.parentNode.insertBefore(node, element);
+ },
+ top: function(element, node) {
+ element.insertBefore(node, element.firstChild);
+ },
+ bottom: function(element, node) {
+ element.appendChild(node);
+ },
+ after: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
+ },
+ tags: {
+ TABLE: ['<table>', '</table>', 1],
+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+ SELECT: ['<select>', '</select>', 1]
+ }
+};
+
+(function() {
+ Object.extend(this.tags, {
+ THEAD: this.tags.TBODY,
+ TFOOT: this.tags.TBODY,
+ TH: this.tags.TD
+ });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ attribute = Element._attributeTranslations.has[attribute] || attribute;
+ var node = $(element).getAttributeNode(attribute);
+ return !!(node && node.specified);
+ }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div')['__proto__']) {
+ window.HTMLElement = { };
+ window.HTMLElement.prototype = document.createElement('div')['__proto__'];
+ Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+ if (Prototype.BrowserFeatures.SpecificElementExtensions)
+ return Prototype.K;
+
+ var Methods = { }, ByTag = Element.Methods.ByTag;
+
+ var extend = Object.extend(function(element) {
+ if (!element || element._extendedByPrototype ||
+ element.nodeType != 1 || element == window) return element;
+
+ var methods = Object.clone(Methods),
+ tagName = element.tagName.toUpperCase(), property, value;
+
+ // extend methods for specific tags
+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+ for (property in methods) {
+ value = methods[property];
+ if (Object.isFunction(value) && !(property in element))
+ element[property] = value.methodize();
+ }
+
+ element._extendedByPrototype = Prototype.emptyFunction;
+ return element;
+
+ }, {
+ refresh: function() {
+ // extend methods for all tags (Safari doesn't need this)
+ if (!Prototype.BrowserFeatures.ElementExtensions) {
+ Object.extend(Methods, Element.Methods);
+ Object.extend(Methods, Element.Methods.Simulated);
+ }
+ }
+ });
+
+ extend.refresh();
+ return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || { });
+ else {
+ if (Object.isArray(tagName)) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = { };
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!Object.isFunction(value)) continue;
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = value.methodize();
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = { };
+ window[klass].prototype = document.createElement(tagName)['__proto__'];
+ return window[klass];
+ }
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (Object.isUndefined(klass)) continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+
+ if (Element.extend.refresh) Element.extend.refresh();
+ Element.cache = { };
+};
+
+document.viewport = {
+ getDimensions: function() {
+ var dimensions = { }, B = Prototype.Browser;
+ $w('width height').each(function(d) {
+ var D = d.capitalize();
+ if (B.WebKit && !document.evaluate) {
+ // Safari <3.0 needs self.innerWidth/Height
+ dimensions[d] = self['inner' + D];
+ } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
+ // Opera <9.5 needs document.body.clientWidth/Height
+ dimensions[d] = document.body['client' + D]
+ } else {
+ dimensions[d] = document.documentElement['client' + D];
+ }
+ });
+ return dimensions;
+ },
+
+ getWidth: function() {
+ return this.getDimensions().width;
+ },
+
+ getHeight: function() {
+ return this.getDimensions().height;
+ },
+
+ getScrollOffsets: function() {
+ return Element._returnOffset(
+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+ }
+};
+/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+ initialize: function(expression) {
+ this.expression = expression.strip();
+
+ if (this.shouldUseSelectorsAPI()) {
+ this.mode = 'selectorsAPI';
+ } else if (this.shouldUseXPath()) {
+ this.mode = 'xpath';
+ this.compileXPathMatcher();
+ } else {
+ this.mode = "normal";
+ this.compileMatcher();
+ }
+
+ },
+
+ shouldUseXPath: function() {
+ if (!Prototype.BrowserFeatures.XPath) return false;
+
+ var e = this.expression;
+
+ // Safari 3 chokes on :*-of-type and :empty
+ if (Prototype.Browser.WebKit &&
+ (e.include("-of-type") || e.include(":empty")))
+ return false;
+
+ // XPath can't do namespaced attributes, nor can it read
+ // the "checked" property from DOM nodes
+ if ((/(\[[\w-]*?:|:checked)/).test(e))
+ return false;
+
+ return true;
+ },
+
+ shouldUseSelectorsAPI: function() {
+ if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
+
+ if (!Selector._div) Selector._div = new Element('div');
+
+ // Make sure the browser treats the selector as valid. Test on an
+ // isolated element to minimize cost of this check.
+ try {
+ Selector._div.querySelector(this.expression);
+ } catch(e) {
+ return false;
+ }
+
+ return true;
+ },
+
+ compileMatcher: function() {
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e];
+ return;
+ }
+
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
+ },
+
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
+
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
+
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
+ },
+
+ findElements: function(root) {
+ root = root || document;
+ var e = this.expression, results;
+
+ switch (this.mode) {
+ case 'selectorsAPI':
+ // querySelectorAll queries document-wide, then filters to descendants
+ // of the context element. That's not what we want.
+ // Add an explicit context to the selector if necessary.
+ if (root !== document) {
+ var oldId = root.id, id = $(root).identify();
+ e = "#" + id + " " + e;
+ }
+
+ results = $A(root.querySelectorAll(e)).map(Element.extend);
+ root.id = oldId;
+
+ return results;
+ case 'xpath':
+ return document._getElementsByXPath(this.xpath, root);
+ default:
+ return this.matcher(root);
+ }
+ },
+
+ match: function(element) {
+ this.tokens = [];
+
+ var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+ var le, p, m;
+
+ while (e && le !== e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ // use the Selector.assertions methods unless the selector
+ // is too complex.
+ if (as[i]) {
+ this.tokens.push([i, Object.clone(m)]);
+ e = e.replace(m[0], '');
+ } else {
+ // reluctantly do a document-wide search
+ // and look for a match in the array
+ return this.findElements(document).include(element);
+ }
+ }
+ }
+ }
+
+ var match = true, name, matches;
+ for (var i = 0, token; token = this.tokens[i]; i++) {
+ name = token[0], matches = token[1];
+ if (!Selector.assertions[name](element, matches)) {
+ match = false; break;
+ }
+ }
+
+ return match;
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
+ }
+});
+
+Object.extend(Selector, {
+ _cache: { },
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: function(m) {
+ m[1] = m[1].toLowerCase();
+ return new Template("[@#{1}]").evaluate(m);
+ },
+ attr: function(m) {
+ m[1] = m[1].toLowerCase();
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (Object.isFunction(h)) return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0)]",
+ 'checked': "[@checked]",
+ 'disabled': "[(@disabled) and (@type!='hidden')]",
+ 'enabled': "[not(@disabled) and (@type!='hidden')]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+ attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ // for Selector.match and Element#match
+ assertions: {
+ tagName: function(element, matches) {
+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
+ },
+
+ className: function(element, matches) {
+ return Element.hasClassName(element, matches[1]);
+ },
+
+ id: function(element, matches) {
+ return element.id === matches[1];
+ },
+
+ attrPresence: function(element, matches) {
+ return Element.hasAttribute(element, matches[1]);
+ },
+
+ attr: function(element, matches) {
+ var nodeValue = Element.readAttribute(element, matches[1]);
+ return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+ }
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ var _true = Prototype.emptyFunction;
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._countedByPrototype = _true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._countedByPrototype = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._countedByPrototype = Prototype.emptyFunction;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ var node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._countedByPrototype) {
+ n._countedByPrototype = Prototype.emptyFunction;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ var uTagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() === uTagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!targetNode) return [];
+ if (!nodes && root == document) return [targetNode];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr, combinator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator, combinator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._countedByPrototype) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || node.firstChild) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._countedByPrototype) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled && (!node.type || node.type !== 'hidden'))
+ results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
+ '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
+ '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
+ '-').include('-' + (v || "").toUpperCase() + '-'); }
+ },
+
+ split: function(expression) {
+ var expressions = [];
+ expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ return expressions;
+ },
+
+ matchElements: function(elements, expression) {
+ var matches = $$(expression), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._countedByPrototype) results.push(element);
+ h.unmark(matches);
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ if (Object.isNumber(expression)) {
+ index = expression; expression = false;
+ }
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ expressions = Selector.split(expressions.join(','));
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
+ }
+});
+
+if (Prototype.Browser.IE) {
+ Object.extend(Selector.handlers, {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ },
+
+ // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node.removeAttribute('_countedByPrototype');
+ return nodes;
+ }
+ });
+}
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, options) {
+ if (typeof options != 'object') options = { hash: !!options };
+ else if (Object.isUndefined(options.hash)) options.hash = true;
+ var key, value, submitted = false, submit = options.submit;
+
+ var data = elements.inject({ }, function(result, element) {
+ if (!element.disabled && element.name) {
+ key = element.name; value = $(element).getValue();
+ if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
+ if (key in result) {
+ // a key is already present; construct an array of values
+ if (!Object.isArray(result[key])) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return options.hash ? data : Object.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, options) {
+ return Form.serializeElements(Form.getElements(form), options);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ var elements = $(form).getElements().findAll(function(element) {
+ return 'hidden' != element.type && !element.disabled;
+ });
+ var firstByIndex = elements.findAll(function(element) {
+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+ }).sortBy(function(element) { return element.tabIndex }).first();
+
+ return firstByIndex ? firstByIndex : elements.find(function(element) {
+ return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || { });
+
+ var params = options.parameters, action = form.readAttribute('action') || '';
+ if (action.blank()) action = window.location.href;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (Object.isString(params)) params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(action, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+};
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = { };
+ pair[element.name] = value;
+ return Object.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ setValue: function(element, value) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ Form.Element.Serializers[method](element, value);
+ return element;
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) { }
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element, value) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element, value);
+ default:
+ return Form.Element.Serializers.textarea(element, value);
+ }
+ },
+
+ inputSelector: function(element, value) {
+ if (Object.isUndefined(value)) return element.checked ? element.value : null;
+ else element.checked = !!value;
+ },
+
+ textarea: function(element, value) {
+ if (Object.isUndefined(value)) return element.value;
+ else element.value = value;
+ },
+
+ select: function(element, value) {
+ if (Object.isUndefined(value))
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ else {
+ var opt, currentValue, single = !Object.isArray(value);
+ for (var i = 0, length = element.length; i < length; i++) {
+ opt = element.options[i];
+ currentValue = this.optionValue(opt);
+ if (single) {
+ if (currentValue == value) {
+ opt.selected = true;
+ return;
+ }
+ }
+ else opt.selected = value.include(currentValue);
+ }
+ }
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+ initialize: function($super, element, frequency, callback) {
+ $super(callback, frequency);
+ this.element = $(element);
+ this.lastValue = this.getValue();
+ },
+
+ execute: function() {
+ var value = this.getValue();
+ if (Object.isString(this.lastValue) && Object.isString(value) ?
+ this.lastValue != value : String(this.lastValue) != String(value)) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback, this);
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) var Event = { };
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+ KEY_INSERT: 45,
+
+ cache: { },
+
+ relatedTarget: function(event) {
+ var element;
+ switch(event.type) {
+ case 'mouseover': element = event.fromElement; break;
+ case 'mouseout': element = event.toElement; break;
+ default: return null;
+ }
+ return Element.extend(element);
+ }
+});
+
+Event.Methods = (function() {
+ var isButton;
+
+ if (Prototype.Browser.IE) {
+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
+ isButton = function(event, code) {
+ return event.button == buttonMap[code];
+ };
+
+ } else if (Prototype.Browser.WebKit) {
+ isButton = function(event, code) {
+ switch (code) {
+ case 0: return event.which == 1 && !event.metaKey;
+ case 1: return event.which == 1 && event.metaKey;
+ default: return false;
+ }
+ };
+
+ } else {
+ isButton = function(event, code) {
+ return event.which ? (event.which === code + 1) : (event.button === code);
+ };
+ }
+
+ return {
+ isLeftClick: function(event) { return isButton(event, 0) },
+ isMiddleClick: function(event) { return isButton(event, 1) },
+ isRightClick: function(event) { return isButton(event, 2) },
+
+ element: function(event) {
+ event = Event.extend(event);
+
+ var node = event.target,
+ type = event.type,
+ currentTarget = event.currentTarget;
+
+ if (currentTarget && currentTarget.tagName) {
+ // Firefox screws up the "click" event when moving between radio buttons
+ // via arrow keys. It also screws up the "load" and "error" events on images,
+ // reporting the document as the target instead of the original image.
+ if (type === 'load' || type === 'error' ||
+ (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
+ && currentTarget.type === 'radio'))
+ node = currentTarget;
+ }
+ if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
+ return Element.extend(node);
+ },
+
+ findElement: function(event, expression) {
+ var element = Event.element(event);
+ if (!expression) return element;
+ var elements = [element].concat(element.ancestors());
+ return Selector.findElement(elements, expression, 0);
+ },
+
+ pointer: function(event) {
+ var docElement = document.documentElement,
+ body = document.body || { scrollLeft: 0, scrollTop: 0 };
+ return {
+ x: event.pageX || (event.clientX +
+ (docElement.scrollLeft || body.scrollLeft) -
+ (docElement.clientLeft || 0)),
+ y: event.pageY || (event.clientY +
+ (docElement.scrollTop || body.scrollTop) -
+ (docElement.clientTop || 0))
+ };
+ },
+
+ pointerX: function(event) { return Event.pointer(event).x },
+ pointerY: function(event) { return Event.pointer(event).y },
+
+ stop: function(event) {
+ Event.extend(event);
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopped = true;
+ }
+ };
+})();
+
+Event.extend = (function() {
+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+ m[name] = Event.Methods[name].methodize();
+ return m;
+ });
+
+ if (Prototype.Browser.IE) {
+ Object.extend(methods, {
+ stopPropagation: function() { this.cancelBubble = true },
+ preventDefault: function() { this.returnValue = false },
+ inspect: function() { return "[object Event]" }
+ });
+
+ return function(event) {
+ if (!event) return false;
+ if (event._extendedByPrototype) return event;
+
+ event._extendedByPrototype = Prototype.emptyFunction;
+ var pointer = Event.pointer(event);
+ Object.extend(event, {
+ target: event.srcElement,
+ relatedTarget: Event.relatedTarget(event),
+ pageX: pointer.x,
+ pageY: pointer.y
+ });
+ return Object.extend(event, methods);
+ };
+
+ } else {
+ Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
+ Object.extend(Event.prototype, methods);
+ return Prototype.K;
+ }
+})();
+
+Object.extend(Event, (function() {
+ var cache = Event.cache;
+
+ function getEventID(element) {
+ if (element._prototypeEventID) return element._prototypeEventID[0];
+ arguments.callee.id = arguments.callee.id || 1;
+ return element._prototypeEventID = [++arguments.callee.id];
+ }
+
+ function getDOMEventName(eventName) {
+ if (eventName && eventName.include(':')) return "dataavailable";
+ return eventName;
+ }
+
+ function getCacheForID(id) {
+ return cache[id] = cache[id] || { };
+ }
+
+ function getWrappersForEventName(id, eventName) {
+ var c = getCacheForID(id);
+ return c[eventName] = c[eventName] || [];
+ }
+
+ function createWrapper(element, eventName, handler) {
+ var id = getEventID(element);
+ var c = getWrappersForEventName(id, eventName);
+ if (c.pluck("handler").include(handler)) return false;
+
+ var wrapper = function(event) {
+ if (!Event || !Event.extend ||
+ (event.eventName && event.eventName != eventName))
+ return false;
+
+ Event.extend(event);
+ handler.call(element, event);
+ };
+
+ wrapper.handler = handler;
+ c.push(wrapper);
+ return wrapper;
+ }
+
+ function findWrapper(id, eventName, handler) {
+ var c = getWrappersForEventName(id, eventName);
+ return c.find(function(wrapper) { return wrapper.handler == handler });
+ }
+
+ function destroyWrapper(id, eventName, handler) {
+ var c = getCacheForID(id);
+ if (!c[eventName]) return false;
+ c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+ }
+
+ function destroyCache() {
+ for (var id in cache)
+ for (var eventName in cache[id])
+ cache[id][eventName] = null;
+ }
+
+
+ // Internet Explorer needs to remove event handlers on page unload
+ // in order to avoid memory leaks.
+ if (window.attachEvent) {
+ window.attachEvent("onunload", destroyCache);
+ }
+
+ // Safari has a dummy event handler on page unload so that it won't
+ // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
+ // object when page is returned to via the back button using its bfcache.
+ if (Prototype.Browser.WebKit) {
+ window.addEventListener('unload', Prototype.emptyFunction, false);
+ }
+
+ return {
+ observe: function(element, eventName, handler) {
+ element = $(element);
+ var name = getDOMEventName(eventName);
+
+ var wrapper = createWrapper(element, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.addEventListener) {
+ element.addEventListener(name, wrapper, false);
+ } else {
+ element.attachEvent("on" + name, wrapper);
+ }
+
+ return element;
+ },
+
+ stopObserving: function(element, eventName, handler) {
+ element = $(element);
+ var id = getEventID(element), name = getDOMEventName(eventName);
+
+ if (!handler && eventName) {
+ getWrappersForEventName(id, eventName).each(function(wrapper) {
+ element.stopObserving(eventName, wrapper.handler);
+ });
+ return element;
+
+ } else if (!eventName) {
+ Object.keys(getCacheForID(id)).each(function(eventName) {
+ element.stopObserving(eventName);
+ });
+ return element;
+ }
+
+ var wrapper = findWrapper(id, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, wrapper, false);
+ } else {
+ element.detachEvent("on" + name, wrapper);
+ }
+
+ destroyWrapper(id, eventName, handler);
+
+ return element;
+ },
+
+ fire: function(element, eventName, memo) {
+ element = $(element);
+ if (element == document && document.createEvent && !element.dispatchEvent)
+ element = document.documentElement;
+
+ var event;
+ if (document.createEvent) {
+ event = document.createEvent("HTMLEvents");
+ event.initEvent("dataavailable", true, true);
+ } else {
+ event = document.createEventObject();
+ event.eventType = "ondataavailable";
+ }
+
+ event.eventName = eventName;
+ event.memo = memo || { };
+
+ if (document.createEvent) {
+ element.dispatchEvent(event);
+ } else {
+ element.fireEvent(event.eventType, event);
+ }
+
+ return Event.extend(event);
+ }
+ };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+ fire: Event.fire,
+ observe: Event.observe,
+ stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+ fire: Element.Methods.fire.methodize(),
+ observe: Element.Methods.observe.methodize(),
+ stopObserving: Element.Methods.stopObserving.methodize(),
+ loaded: false
+});
+
+(function() {
+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+ Matthias Miller, Dean Edwards and John Resig. */
+
+ var timer;
+
+ function fireContentLoadedEvent() {
+ if (document.loaded) return;
+ if (timer) window.clearInterval(timer);
+ document.fire("dom:loaded");
+ document.loaded = true;
+ }
+
+ if (document.addEventListener) {
+ if (Prototype.Browser.WebKit) {
+ timer = window.setInterval(function() {
+ if (/loaded|complete/.test(document.readyState))
+ fireContentLoadedEvent();
+ }, 0);
+
+ Event.observe(window, "load", fireContentLoadedEvent);
+
+ } else {
+ document.addEventListener("DOMContentLoaded",
+ fireContentLoadedEvent, false);
+ }
+
+ } else {
+ document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+ $("__onDOMContentLoaded").onreadystatechange = function() {
+ if (this.readyState == "complete") {
+ this.onreadystatechange = null;
+ fireContentLoadedEvent();
+ }
+ };
+ }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+ Before: function(element, content) {
+ return Element.insert(element, {before:content});
+ },
+
+ Top: function(element, content) {
+ return Element.insert(element, {top:content});
+ },
+
+ Bottom: function(element, content) {
+ return Element.insert(element, {bottom:content});
+ },
+
+ After: function(element, content) {
+ return Element.insert(element, {after:content});
+ }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = Element.cumulativeScrollOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ // Deprecation layer -- use newer Element methods now (1.5.2).
+
+ cumulativeOffset: Element.Methods.cumulativeOffset,
+
+ positionedOffset: Element.Methods.positionedOffset,
+
+ absolutize: function(element) {
+ Position.prepare();
+ return Element.absolutize(element);
+ },
+
+ relativize: function(element) {
+ Position.prepare();
+ return Element.relativize(element);
+ },
+
+ realOffset: Element.Methods.cumulativeScrollOffset,
+
+ offsetParent: Element.Methods.getOffsetParent,
+
+ page: Element.Methods.viewportOffset,
+
+ clone: function(source, target, options) {
+ options = options || { };
+ return Element.clonePosition(target, source, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+ function iter(name) {
+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+ }
+
+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+ function(element, className) {
+ className = className.toString().strip();
+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+ } : function(element, className) {
+ className = className.toString().strip();
+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+ if (!classNames && !className) return elements;
+
+ var nodes = $(element).getElementsByTagName('*');
+ className = ' ' + className + ' ';
+
+ for (var i = 0, child, cn; child = nodes[i]; i++) {
+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+ (classNames && classNames.all(function(name) {
+ return !name.toString().blank() && cn.include(' ' + name + ' ');
+ }))))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+ };
+
+ return function(className, parentElement) {
+ return $(parentElement || document.body).getElementsByClassName(className);
+ };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods();
\ No newline at end of file
--- /dev/null
+Event.observe(window,'load',function() {
+ /*
+ If we're viewing a tag or branch, don't display it in the
+ revision box
+ */
+ var branch_selected = $('branch') && $('rev').getValue() == $('branch').getValue();
+ var tag_selected = $('tag') && $('rev').getValue() == $('tag').getValue();
+ if (branch_selected || tag_selected) {
+ $('rev').setValue('');
+ }
+
+ /*
+ Copy the branch/tag value into the revision box, then disable
+ the dropdowns before submitting the form
+ */
+ $$('#branch,#tag').each(function(e) {
+ e.observe('change',function(e) {
+ $('rev').setValue(e.element().getValue());
+ $$('#branch,#tag').invoke('disable');
+ e.element().parentNode.submit();
+ $$('#branch,#tag').invoke('enable');
+ });
+ });
+
+ /*
+ Disable the branch/tag dropdowns before submitting the revision form
+ */
+ $('rev').observe('keydown', function(e) {
+ if (e.keyCode == 13) {
+ $$('#branch,#tag').invoke('disable');
+ e.element().parentNode.submit();
+ $$('#branch,#tag').invoke('enable');
+ }
+ });
+})
--- /dev/null
+var NS4 = (navigator.appName == "Netscape" && parseInt(navigator.appVersion) < 5);\r
+\r
+function addOption(theSel, theText, theValue)\r
+{\r
+ var newOpt = new Option(theText, theValue);\r
+ var selLength = theSel.length;\r
+ theSel.options[selLength] = newOpt;\r
+}\r
+\r
+function deleteOption(theSel, theIndex)\r
+{ \r
+ var selLength = theSel.length;\r
+ if(selLength>0)\r
+ {\r
+ theSel.options[theIndex] = null;\r
+ }\r
+}\r
+\r
+function moveOptions(theSelFrom, theSelTo)\r
+{\r
+ \r
+ var selLength = theSelFrom.length;\r
+ var selectedText = new Array();\r
+ var selectedValues = new Array();\r
+ var selectedCount = 0;\r
+ \r
+ var i;\r
+ \r
+ for(i=selLength-1; i>=0; i--)\r
+ {\r
+ if(theSelFrom.options[i].selected)\r
+ {\r
+ selectedText[selectedCount] = theSelFrom.options[i].text;\r
+ selectedValues[selectedCount] = theSelFrom.options[i].value;\r
+ deleteOption(theSelFrom, i);\r
+ selectedCount++;\r
+ }\r
+ }\r
+ \r
+ for(i=selectedCount-1; i>=0; i--)\r
+ {\r
+ addOption(theSelTo, selectedText[i], selectedValues[i]);\r
+ }\r
+ \r
+ if(NS4) history.go(0);\r
+}\r
+\r
+function selectAllOptions(id)\r
+{\r
+ var select = $(id);\r
+ for (var i=0; i<select.options.length; i++) {\r
+ select.options[i].selected = true;\r
+ }\r
+}\r
+\r
--- /dev/null
+body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
+
+h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
+h1 {margin:0; padding:0; font-size: 24px;}
+h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
+h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
+h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
+
+/***** Layout *****/
+#wrapper {background: white;}
+
+#top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
+#top-menu ul {margin: 0; padding: 0;}
+#top-menu li {
+ float:left;
+ list-style-type:none;
+ margin: 0px 0px 0px 0px;
+ padding: 0px 0px 0px 0px;
+ white-space:nowrap;
+}
+#top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
+#top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
+
+#account {float:right;}
+
+#header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
+#header a {color:#f8f8f8;}
+#header h1 a.ancestor { font-size: 80%; }
+#quick-search {float:right;}
+
+#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
+#main-menu ul {margin: 0; padding: 0;}
+#main-menu li {
+ float:left;
+ list-style-type:none;
+ margin: 0px 2px 0px 0px;
+ padding: 0px 0px 0px 0px;
+ white-space:nowrap;
+}
+#main-menu li a {
+ display: block;
+ color: #fff;
+ text-decoration: none;
+ font-weight: bold;
+ margin: 0;
+ padding: 4px 10px 4px 10px;
+}
+#main-menu li a:hover {background:#759FCF; color:#fff;}
+#main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
+
+#main {background-color:#EEEEEE;}
+
+#sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
+* html #sidebar{ width: 17%; }
+#sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
+#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
+* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
+
+#content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
+* html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
+html>body #content { min-height: 600px; }
+* html body #content { height: 600px; } /* IE */
+
+#main.nosidebar #sidebar{ display: none; }
+#main.nosidebar #content{ width: auto; border-right: 0; }
+
+#footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
+
+#login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
+#login-form table td {padding: 6px;}
+#login-form label {font-weight: bold;}
+#login-form input#username, #login-form input#password { width: 300px; }
+
+input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
+
+.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
+
+/***** Links *****/
+a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
+a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
+a img{ border: 0; }
+
+a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
+
+/***** Tables *****/
+table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
+table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
+table.list td { vertical-align: top; }
+table.list td.id { width: 2%; text-align: center;}
+table.list td.checkbox { width: 15px; padding: 0px;}
+table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
+table.list td.buttons a { padding-right: 0.6em; }
+
+tr.project td.name a { padding-left: 16px; white-space:nowrap; }
+tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
+
+tr.issue { text-align: center; white-space: nowrap; }
+tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
+tr.issue td.subject { text-align: left; }
+tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
+
+tr.entry { border: 1px solid #f8f8f8; }
+tr.entry td { white-space: nowrap; }
+tr.entry td.filename { width: 30%; }
+tr.entry td.size { text-align: right; font-size: 90%; }
+tr.entry td.revision, tr.entry td.author { text-align: center; }
+tr.entry td.age { text-align: right; }
+tr.entry.file td.filename a { margin-left: 16px; }
+
+tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
+tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
+
+tr.changeset td.author { text-align: center; width: 15%; }
+tr.changeset td.committed_on { text-align: center; width: 15%; }
+
+table.files tr.file td { text-align: center; }
+table.files tr.file td.filename { text-align: left; padding-left: 24px; }
+table.files tr.file td.digest { font-size: 80%; }
+
+table.members td.roles, table.memberships td.roles { width: 45%; }
+
+tr.message { height: 2.6em; }
+tr.message td.last_message { font-size: 80%; }
+tr.message.locked td.subject a { background-image: url(../images/locked.png); }
+tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
+
+tr.version.closed, tr.version.closed a { color: #999; }
+
+tr.user td { width:13%; }
+tr.user td.email { width:18%; }
+tr.user td { white-space: nowrap; }
+tr.user.locked, tr.user.registered { color: #aaa; }
+tr.user.locked a, tr.user.registered a { color: #aaa; }
+
+tr.time-entry { text-align: center; white-space: nowrap; }
+tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
+td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
+td.hours .hours-dec { font-size: 0.9em; }
+
+table.plugins td { vertical-align: middle; }
+table.plugins td.configure { text-align: right; padding-right: 1em; }
+table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
+table.plugins span.description { display: block; font-size: 0.9em; }
+table.plugins span.url { display: block; font-size: 0.9em; }
+
+table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
+table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
+
+table.list tbody tr:hover { background-color:#ffffdd; }
+table.list tbody tr.group:hover { background-color:inherit; }
+table td {padding:2px;}
+table p {margin:0;}
+.odd {background-color:#f6f7f8;}
+.even {background-color: #fff;}
+
+a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
+a.sort.asc { background-image: url(../images/sort_asc.png); }
+a.sort.desc { background-image: url(../images/sort_desc.png); }
+
+table.attributes { width: 100% }
+table.attributes th { vertical-align: top; text-align: left; }
+table.attributes td { vertical-align: top; }
+
+td.center {text-align:center;}
+
+.highlight { background-color: #FCFD8D;}
+.highlight.token-1 { background-color: #faa;}
+.highlight.token-2 { background-color: #afa;}
+.highlight.token-3 { background-color: #aaf;}
+
+.box{
+padding:6px;
+margin-bottom: 10px;
+background-color:#f6f6f6;
+color:#505050;
+line-height:1.5em;
+border: 1px solid #e4e4e4;
+}
+
+div.square {
+ border: 1px solid #999;
+ float: left;
+ margin: .3em .4em 0 .4em;
+ overflow: hidden;
+ width: .6em; height: .6em;
+}
+.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
+.contextual input, .contextual select {font-size:0.9em;}
+.message .contextual { margin-top: 0; }
+
+.splitcontentleft{float:left; width:49%;}
+.splitcontentright{float:right; width:49%;}
+form {display: inline;}
+input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
+fieldset {border: 1px solid #e4e4e4; margin:0;}
+legend {color: #484848;}
+hr { width: 100%; height: 1px; background: #ccc; border: 0;}
+blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
+blockquote blockquote { margin-left: 0;}
+acronym { border-bottom: 1px dotted; cursor: help; }
+textarea.wiki-edit { width: 99%; }
+li p {margin-top: 0;}
+div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
+p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
+p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
+p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
+
+fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
+fieldset.collapsible legend { 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; }
+fieldset#filters table td { padding: 0; vertical-align: middle; }
+fieldset#filters tr.filter { height: 2em; }
+fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
+.buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
+
+div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
+div#issue-changesets .changeset { padding: 4px;}
+div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
+div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
+
+div#activity dl, #search-results { margin-left: 2em; }
+div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
+div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
+div#activity dt.me .time { border-bottom: 1px solid #999; }
+div#activity dt .time { color: #777; font-size: 80%; }
+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; }
+
+#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
+
+div#search-results-counts {float:right;}
+div#search-results-counts ul { margin-top: 0.5em; }
+div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
+
+dt.issue { background-image: url(../images/ticket.png); }
+dt.issue-edit { background-image: url(../images/ticket_edit.png); }
+dt.issue-closed { background-image: url(../images/ticket_checked.png); }
+dt.issue-note { background-image: url(../images/ticket_note.png); }
+dt.changeset { background-image: url(../images/changeset.png); }
+dt.news { background-image: url(../images/news.png); }
+dt.message { background-image: url(../images/message.png); }
+dt.reply { background-image: url(../images/comments.png); }
+dt.wiki-page { background-image: url(../images/wiki_edit.png); }
+dt.attachment { background-image: url(../images/attachment.png); }
+dt.document { background-image: url(../images/document.png); }
+dt.project { background-image: url(../images/projects.png); }
+dt.time-entry { background-image: url(../images/time.png); }
+
+#search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
+
+div#roadmap fieldset.related-issues { margin-bottom: 1em; }
+div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
+div#roadmap .wiki h1:first-child { display: none; }
+div#roadmap .wiki h1 { font-size: 120%; }
+div#roadmap .wiki h2 { font-size: 110%; }
+
+div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
+div#version-summary fieldset { margin-bottom: 1em; }
+div#version-summary .total-hours { text-align: right; }
+
+table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
+table#time-report tbody tr { font-style: italic; color: #777; }
+table#time-report tbody tr.last-level { font-style: normal; color: #555; }
+table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
+table#time-report .hours-dec { font-size: 0.9em; }
+
+form#issue-form .attributes { margin-bottom: 8px; }
+form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
+form#issue-form .attributes select { min-width: 30%; }
+
+ul.projects { margin: 0; padding-left: 1em; }
+ul.projects.root { margin: 0; padding: 0; }
+ul.projects ul { border-left: 3px solid #e0e0e0; }
+ul.projects li { list-style-type:none; }
+ul.projects li.root { margin-bottom: 1em; }
+ul.projects li.child { margin-top: 1em;}
+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%; }
+
+#tracker_project_ids ul { margin: 0; padding-left: 1em; }
+#tracker_project_ids li { list-style-type:none; }
+
+ul.properties {padding:0; font-size: 0.9em; color: #777;}
+ul.properties li {list-style-type:none;}
+ul.properties li span {font-style:italic;}
+
+.total-hours { font-size: 110%; font-weight: bold; }
+.total-hours span.hours-int { font-size: 120%; }
+
+.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
+#user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
+
+.pagination {font-size: 90%}
+p.pagination {margin-top:8px;}
+
+/***** Tabular forms ******/
+.tabular p{
+margin: 0;
+padding: 5px 0 8px 0;
+padding-left: 180px; /*width of left column containing the label elements*/
+height: 1%;
+clear:left;
+}
+
+html>body .tabular p {overflow:hidden;}
+
+.tabular label{
+font-weight: bold;
+float: left;
+text-align: right;
+margin-left: -180px; /*width of left column*/
+width: 175px; /*width of labels. Should be smaller than left column to create some right
+margin*/
+}
+
+.tabular label.floating{
+font-weight: normal;
+margin-left: 0px;
+text-align: left;
+width: 270px;
+}
+
+.tabular label.block{
+font-weight: normal;
+margin-left: 0px !important;
+text-align: left;
+float: none;
+display: block;
+width: auto;
+}
+
+input#time_entry_comments { width: 90%;}
+
+#preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
+
+.tabular.settings p{ padding-left: 300px; }
+.tabular.settings label{ margin-left: -300px; width: 295px; }
+
+.required {color: #bb0000;}
+.summary {font-style: italic;}
+
+#attachments_fields input[type=text] {margin-left: 8px; }
+
+div.attachments { margin-top: 12px; }
+div.attachments p { margin:4px 0 2px 0; }
+div.attachments img { vertical-align: middle; }
+div.attachments span.author { font-size: 0.9em; color: #888; }
+
+p.other-formats { text-align: right; font-size:0.9em; color: #666; }
+.other-formats span + span:before { content: "| "; }
+
+a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
+
+/* 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; }
+
+table.members td.group { padding-left: 20px; background: url(../images/users.png) no-repeat 0% 0%; }
+
+* html div#tab-content-members fieldset div { height: 450px; }
+
+/***** Flash & error messages ****/
+#errorExplanation, div.flash, .nodata, .warning {
+ padding: 4px 4px 4px 30px;
+ margin-bottom: 12px;
+ font-size: 1.1em;
+ border: 2px solid;
+}
+
+div.flash {margin-top: 8px;}
+
+div.flash.error, #errorExplanation {
+ background: url(../images/false.png) 8px 5px no-repeat;
+ background-color: #ffe3e3;
+ border-color: #dd0000;
+ color: #550000;
+}
+
+div.flash.notice {
+ background: url(../images/true.png) 8px 5px no-repeat;
+ background-color: #dfffdf;
+ border-color: #9fcf9f;
+ color: #005f00;
+}
+
+div.flash.warning {
+ background: url(../images/warning.png) 8px 5px no-repeat;
+ background-color: #FFEBC1;
+ border-color: #FDBF3B;
+ color: #A6750C;
+ text-align: left;
+}
+
+.nodata, .warning {
+ text-align: center;
+ background-color: #FFEBC1;
+ border-color: #FDBF3B;
+ color: #A6750C;
+}
+
+#errorExplanation ul { font-size: 0.9em;}
+#errorExplanation h2, #errorExplanation p { display: none; }
+
+/***** Ajax indicator ******/
+#ajax-indicator {
+position: absolute; /* fixed not supported by IE */
+background-color:#eee;
+border: 1px solid #bbb;
+top:35%;
+left:40%;
+width:20%;
+font-weight:bold;
+text-align:center;
+padding:0.6em;
+z-index:100;
+filter:alpha(opacity=50);
+opacity: 0.5;
+}
+
+html>body #ajax-indicator { position: fixed; }
+
+#ajax-indicator span {
+background-position: 0% 40%;
+background-repeat: no-repeat;
+background-image: url(../images/loading.gif);
+padding-left: 26px;
+vertical-align: bottom;
+}
+
+/***** Calendar *****/
+table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
+table.cal thead th {width: 14%;}
+table.cal tbody tr {height: 100px;}
+table.cal th { background-color:#EEEEEE; padding: 4px; }
+table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
+table.cal td p.day-num {font-size: 1.1em; text-align:right;}
+table.cal td.odd p.day-num {color: #bbb;}
+table.cal td.today {background:#ffffdd;}
+table.cal td.today p.day-num {font-weight: bold;}
+
+/***** Tooltips ******/
+.tooltip{position:relative;z-index:24;}
+.tooltip:hover{z-index:25;color:#000;}
+.tooltip span.tip{display: none; text-align:left;}
+
+div.tooltip:hover span.tip{
+display:block;
+position:absolute;
+top:12px; left:24px; width:270px;
+border:1px solid #555;
+background-color:#fff;
+padding: 4px;
+font-size: 0.8em;
+color:#505050;
+}
+
+/***** Progress bar *****/
+table.progress {
+ border: 1px solid #D7D7D7;
+ border-collapse: collapse;
+ border-spacing: 0pt;
+ empty-cells: show;
+ text-align: center;
+ float:left;
+ margin: 1px 6px 1px 0px;
+}
+
+table.progress td { height: 0.9em; }
+table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
+table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
+table.progress td.open { background: #FFF none repeat scroll 0%; }
+p.pourcent {font-size: 80%;}
+p.progress-info {clear: left; font-style: italic; font-size: 80%;}
+
+/***** Tabs *****/
+#content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
+#content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
+#content .tabs>ul { bottom:-1px; } /* others */
+#content .tabs ul li {
+float:left;
+list-style-type:none;
+white-space:nowrap;
+margin-right:8px;
+background:#fff;
+}
+#content .tabs ul li a{
+display:block;
+font-size: 0.9em;
+text-decoration:none;
+line-height:1.3em;
+padding:4px 6px 4px 6px;
+border: 1px solid #ccc;
+border-bottom: 1px solid #bbbbbb;
+background-color: #eeeeee;
+color:#777;
+font-weight:bold;
+}
+
+#content .tabs ul li a:hover {
+background-color: #ffffdd;
+text-decoration:none;
+}
+
+#content .tabs ul li a.selected {
+background-color: #fff;
+border: 1px solid #bbbbbb;
+border-bottom: 1px solid #fff;
+}
+
+#content .tabs ul li a.selected:hover {
+background-color: #fff;
+}
+
+/***** Auto-complete *****/
+div.autocomplete {
+ position:absolute;
+ width:250px;
+ background-color:white;
+ margin:0;
+ padding:0;
+}
+div.autocomplete ul {
+ list-style-type:none;
+ margin:0;
+ padding:0;
+}
+div.autocomplete ul li.selected { background-color: #ffb;}
+div.autocomplete ul li {
+ list-style-type:none;
+ display:block;
+ margin:0;
+ padding:2px;
+ cursor:pointer;
+ font-size: 90%;
+ border-bottom: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+div.autocomplete ul li span.informal {
+ font-size: 80%;
+ color: #aaa;
+}
+
+/***** Diff *****/
+.diff_out { background: #fcc; }
+.diff_in { background: #cfc; }
+
+/***** Wiki *****/
+div.wiki table {
+ border: 1px solid #505050;
+ border-collapse: collapse;
+ margin-bottom: 1em;
+}
+
+div.wiki table, div.wiki td, div.wiki th {
+ border: 1px solid #bbb;
+ padding: 4px;
+}
+
+div.wiki .external {
+ background-position: 0% 60%;
+ background-repeat: no-repeat;
+ padding-left: 12px;
+ background-image: url(../images/external.png);
+}
+
+div.wiki a.new {
+ color: #b73535;
+}
+
+div.wiki pre {
+ margin: 1em 1em 1em 1.6em;
+ padding: 2px;
+ background-color: #fafafa;
+ border: 1px solid #dadada;
+ width:95%;
+ overflow-x: auto;
+}
+
+div.wiki ul.toc {
+ background-color: #ffffdd;
+ border: 1px solid #e4e4e4;
+ padding: 4px;
+ line-height: 1.2em;
+ margin-bottom: 12px;
+ margin-right: 12px;
+ margin-left: 0;
+ display: table
+}
+* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
+
+div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
+div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
+div.wiki ul.toc li { list-style-type:none;}
+div.wiki ul.toc li.heading2 { margin-left: 6px; }
+div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
+
+div.wiki ul.toc a {
+ font-size: 0.9em;
+ font-weight: normal;
+ text-decoration: none;
+ color: #606060;
+}
+div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
+
+a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
+a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
+h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
+
+/***** My page layout *****/
+.block-receiver {
+border:1px dashed #c0c0c0;
+margin-bottom: 20px;
+padding: 15px 0 15px 0;
+}
+
+.mypage-box {
+margin:0 0 20px 0;
+color:#505050;
+line-height:1.5em;
+}
+
+.handle {
+cursor: move;
+}
+
+a.close-icon {
+display:block;
+margin-top:3px;
+overflow:hidden;
+width:12px;
+height:12px;
+background-repeat: no-repeat;
+cursor:pointer;
+background-image:url('../images/close.png');
+}
+
+a.close-icon:hover {
+background-image:url('../images/close_hl.png');
+}
+
+/***** Gantt chart *****/
+.gantt_hdr {
+ position:absolute;
+ top:0;
+ height:16px;
+ border-top: 1px solid #c0c0c0;
+ border-bottom: 1px solid #c0c0c0;
+ border-right: 1px solid #c0c0c0;
+ text-align: center;
+ overflow: hidden;
+}
+
+.task {
+ position: absolute;
+ height:8px;
+ font-size:0.8em;
+ color:#888;
+ padding:0;
+ margin:0;
+ line-height:0.8em;
+}
+
+.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
+.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
+.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
+.milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
+
+/***** Icons *****/
+.icon {
+background-position: 0% 40%;
+background-repeat: no-repeat;
+padding-left: 20px;
+padding-top: 2px;
+padding-bottom: 3px;
+}
+
+.icon22 {
+background-position: 0% 40%;
+background-repeat: no-repeat;
+padding-left: 26px;
+line-height: 22px;
+vertical-align: middle;
+}
+
+.icon-add { background-image: url(../images/add.png); }
+.icon-edit { background-image: url(../images/edit.png); }
+.icon-copy { background-image: url(../images/copy.png); }
+.icon-del { background-image: url(../images/delete.png); }
+.icon-move { background-image: url(../images/move.png); }
+.icon-save { background-image: url(../images/save.png); }
+.icon-cancel { background-image: url(../images/cancel.png); }
+.icon-folder { background-image: url(../images/folder.png); }
+.open .icon-folder { background-image: url(../images/folder_open.png); }
+.icon-package { background-image: url(../images/package.png); }
+.icon-home { background-image: url(../images/home.png); }
+.icon-user { background-image: url(../images/user.png); }
+.icon-mypage { background-image: url(../images/user_page.png); }
+.icon-admin { background-image: url(../images/admin.png); }
+.icon-projects { background-image: url(../images/projects.png); }
+.icon-help { background-image: url(../images/help.png); }
+.icon-attachment { background-image: url(../images/attachment.png); }
+.icon-index { background-image: url(../images/index.png); }
+.icon-history { background-image: url(../images/history.png); }
+.icon-time { background-image: url(../images/time.png); }
+.icon-time-add { background-image: url(../images/time_add.png); }
+.icon-stats { background-image: url(../images/stats.png); }
+.icon-warning { background-image: url(../images/warning.png); }
+.icon-fav { background-image: url(../images/fav.png); }
+.icon-fav-off { background-image: url(../images/fav_off.png); }
+.icon-reload { background-image: url(../images/reload.png); }
+.icon-lock { background-image: url(../images/locked.png); }
+.icon-unlock { background-image: url(../images/unlock.png); }
+.icon-checked { background-image: url(../images/true.png); }
+.icon-details { background-image: url(../images/zoom_in.png); }
+.icon-report { background-image: url(../images/report.png); }
+.icon-comment { background-image: url(../images/comment.png); }
+
+.icon-file { background-image: url(../images/files/default.png); }
+.icon-file.text-plain { background-image: url(../images/files/text.png); }
+.icon-file.text-x-c { background-image: url(../images/files/c.png); }
+.icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
+.icon-file.text-x-php { background-image: url(../images/files/php.png); }
+.icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
+.icon-file.text-xml { background-image: url(../images/files/xml.png); }
+.icon-file.image-gif { background-image: url(../images/files/image.png); }
+.icon-file.image-jpeg { background-image: url(../images/files/image.png); }
+.icon-file.image-png { background-image: url(../images/files/image.png); }
+.icon-file.image-tiff { background-image: url(../images/files/image.png); }
+.icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
+.icon-file.application-zip { background-image: url(../images/files/zip.png); }
+.icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
+
+.icon22-projects { background-image: url(../images/22x22/projects.png); }
+.icon22-users { background-image: url(../images/22x22/users.png); }
+.icon22-groups { background-image: url(../images/22x22/groups.png); }
+.icon22-tracker { background-image: url(../images/22x22/tracker.png); }
+.icon22-role { background-image: url(../images/22x22/role.png); }
+.icon22-workflow { background-image: url(../images/22x22/workflow.png); }
+.icon22-options { background-image: url(../images/22x22/options.png); }
+.icon22-notifications { background-image: url(../images/22x22/notifications.png); }
+.icon22-authent { background-image: url(../images/22x22/authent.png); }
+.icon22-info { background-image: url(../images/22x22/info.png); }
+.icon22-comment { background-image: url(../images/22x22/comment.png); }
+.icon22-package { background-image: url(../images/22x22/package.png); }
+.icon22-settings { background-image: url(../images/22x22/settings.png); }
+.icon22-plugin { background-image: url(../images/22x22/plugin.png); }
+
+img.gravatar {
+ padding: 2px;
+ border: solid 1px #d5d5d5;
+ background: #fff;
+}
+
+div.issue img.gravatar {
+ float: right;
+ margin: 0 0 0 1em;
+ padding: 5px;
+}
+
+div.issue table img.gravatar {
+ height: 14px;
+ width: 14px;
+ padding: 2px;
+ float: left;
+ margin: 0 0.5em 0 0;
+}
+
+#history img.gravatar {
+ padding: 3px;
+ margin: 0 1.5em 1em 0;
+ float: left;
+}
+
+td.username img.gravatar {
+ float: left;
+ margin: 0 1em 0 0;
+}
+
+#activity dt img.gravatar {
+ float: left;
+ margin: 0 1em 1em 0;
+}
+
+#activity dt,
+.journal {
+ clear: left;
+}
+
+.gravatar-margin {
+ margin-left: 40px;
+}
+
+h2 img { vertical-align:middle; }
+
+
+/***** Media print specific styles *****/
+@media print {
+ #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
+ #main { background: #fff; }
+ #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
+ #wiki_add_attachment { display:none; }
+}
--- /dev/null
+/* The main calendar widget. DIV containing a table. */
+
+img.calendar-trigger {
+ cursor: pointer;
+ vertical-align: middle;
+ margin-left: 4px;
+}
+
+div.calendar { position: relative; z-index: 30;}
+
+.calendar, .calendar table {
+ border: 1px solid #556;
+ font-size: 11px;
+ color: #000;
+ cursor: default;
+ background: #fafbfc;
+ font-family: tahoma,verdana,sans-serif;
+}
+
+/* Header part -- contains navigation buttons and day names. */
+
+.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */
+ text-align: center; /* They are the navigation buttons */
+ padding: 2px; /* Make the buttons seem like they're pressing */
+}
+
+.calendar .nav {
+ background: #467aa7;
+}
+
+.calendar thead .title { /* This holds the current "month, year" */
+ font-weight: bold; /* Pressing it will take you to the current date */
+ text-align: center;
+ background: #fff;
+ color: #000;
+ padding: 2px;
+}
+
+.calendar thead .headrow { /* Row <TR> containing navigation buttons */
+ background: #467aa7;
+ color: #fff;
+}
+
+.calendar thead .daynames { /* Row <TR> containing the day names */
+ background: #bdf;
+}
+
+.calendar thead .name { /* Cells <TD> containing the day names */
+ border-bottom: 1px solid #556;
+ padding: 2px;
+ text-align: center;
+ color: #000;
+}
+
+.calendar thead .weekend { /* How a weekend day name shows in header */
+ color: #a66;
+}
+
+.calendar thead .hilite { /* How do the buttons in header appear when hover */
+ background-color: #80b0da;
+ color: #000;
+ padding: 1px;
+}
+
+.calendar thead .active { /* Active (pressed) buttons in header */
+ background-color: #77c;
+ padding: 2px 0px 0px 2px;
+}
+
+/* The body part -- contains all the days in month. */
+
+.calendar tbody .day { /* Cells <TD> containing month days dates */
+ width: 2em;
+ color: #456;
+ text-align: right;
+ padding: 2px 4px 2px 2px;
+}
+.calendar tbody .day.othermonth {
+ font-size: 80%;
+ color: #bbb;
+}
+.calendar tbody .day.othermonth.oweekend {
+ color: #fbb;
+}
+
+.calendar table .wn {
+ padding: 2px 3px 2px 2px;
+ border-right: 1px solid #000;
+ background: #bdf;
+}
+
+.calendar tbody .rowhilite td {
+ background: #def;
+}
+
+.calendar tbody .rowhilite td.wn {
+ background: #80b0da;
+}
+
+.calendar tbody td.hilite { /* Hovered cells <TD> */
+ background: #80b0da;
+ padding: 1px 3px 1px 1px;
+ border: 1px solid #bbb;
+}
+
+.calendar tbody td.active { /* Active (pressed) cells <TD> */
+ background: #cde;
+ padding: 2px 2px 0px 2px;
+}
+
+.calendar tbody td.selected { /* Cell showing today date */
+ font-weight: bold;
+ border: 1px solid #000;
+ padding: 1px 3px 1px 1px;
+ background: #fff;
+ color: #000;
+}
+
+.calendar tbody td.weekend { /* Cells showing weekend days */
+ color: #a66;
+}
+
+.calendar tbody td.today { /* Cell showing selected date */
+ font-weight: bold;
+ color: #f00;
+}
+
+.calendar tbody .disabled { color: #999; }
+
+.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */
+ visibility: hidden;
+}
+
+.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */
+ display: none;
+}
+
+/* The footer part -- status bar and "Close" button */
+
+.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */
+ text-align: center;
+ background: #556;
+ color: #fff;
+}
+
+.calendar tfoot .ttip { /* Tooltip (status bar) cell <TD> */
+ background: #fff;
+ color: #445;
+ border-top: 1px solid #556;
+ padding: 1px;
+}
+
+.calendar tfoot .hilite { /* Hover style for buttons in footer */
+ background: #aaf;
+ border: 1px solid #04f;
+ color: #000;
+ padding: 1px;
+}
+
+.calendar tfoot .active { /* Active (pressed) style for buttons in footer */
+ background: #77c;
+ padding: 2px 0px 0px 2px;
+}
+
+/* Combo boxes (menus that display months/years for direct selection) */
+
+.calendar .combo {
+ position: absolute;
+ display: none;
+ top: 0px;
+ left: 0px;
+ width: 4em;
+ cursor: default;
+ border: 1px solid #655;
+ background: #def;
+ color: #000;
+ font-size: 90%;
+ z-index: 100;
+}
+
+.calendar .combo .label,
+.calendar .combo .label-IEfix {
+ text-align: center;
+ padding: 1px;
+}
+
+.calendar .combo .label-IEfix {
+ width: 4em;
+}
+
+.calendar .combo .hilite {
+ background: #acf;
+}
+
+.calendar .combo .active {
+ border-top: 1px solid #46a;
+ border-bottom: 1px solid #46a;
+ background: #eef;
+ font-weight: bold;
+}
+
+.calendar td.time {
+ border-top: 1px solid #000;
+ padding: 1px 0px;
+ text-align: center;
+ background-color: #f4f0e8;
+}
+
+.calendar td.time .hour,
+.calendar td.time .minute,
+.calendar td.time .ampm {
+ padding: 0px 3px 0px 4px;
+ border: 1px solid #889;
+ font-weight: bold;
+ background-color: #fff;
+}
+
+.calendar td.time .ampm {
+ text-align: center;
+}
+
+.calendar td.time .colon {
+ padding: 0px 2px 0px 3px;
+ font-weight: bold;
+}
+
+.calendar td.time span.hilite {
+ border-color: #000;
+ background-color: #667;
+ color: #fff;
+}
+
+.calendar td.time span.active {
+ border-color: #f00;
+ background-color: #000;
+ color: #0f0;
+}
--- /dev/null
+#context-menu { position: absolute; z-index: 40; font-size: 0.9em;}
+
+#context-menu ul, #context-menu li, #context-menu a {
+ display:block;
+ margin:0;
+ padding:0;
+ border:0;
+}
+
+#context-menu ul {
+ width:150px;
+ border-top:1px solid #ddd;
+ border-left:1px solid #ddd;
+ border-bottom:1px solid #777;
+ border-right:1px solid #777;
+ background:white;
+ list-style:none;
+}
+
+#context-menu li {
+ position:relative;
+ padding:1px;
+ z-index:39;
+}
+#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; }
+#context-menu li.folder>ul { left:148px; }
+
+#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; }
+#context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ }
+#context-menu.reverse-x li.folder>ul { right:148px; }
+
+#context-menu a {
+ border:1px solid white;
+ text-decoration:none !important;
+ background-repeat: no-repeat;
+ background-position: 1px 50%;
+ padding: 1px 0px 1px 20px;
+ width:100%; /* IE */
+}
+#context-menu li>a { width:auto; } /* others */
+#context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;}
+#context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; }
+#context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; }
+#context-menu li.folder a:hover { background-color:#eee; }
+#context-menu li.folder:hover { z-index:40; }
+#context-menu ul ul, #context-menu li:hover ul ul { display:none; }
+#context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
+
+/* selected element */
+.context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
+.context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
+.context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; }
--- /dev/null
+<attach event="ondocumentready" handler="parseStylesheets" />\r
+<script>\r
+/**\r
+ * Whatever:hover - V1.42.060206 - hover & active\r
+ * ------------------------------------------------------------\r
+ * (c) 2005 - Peter Nederlof\r
+ * Peterned - http://www.xs4all.nl/~peterned/\r
+ * License - http://creativecommons.org/licenses/LGPL/2.1/\r
+ *\r
+ * Whatever:hover is free software; you can redistribute it and/or\r
+ * modify it under the terms of the GNU Lesser General Public\r
+ * License as published by the Free Software Foundation; either\r
+ * version 2.1 of the License, or (at your option) any later version.\r
+ *\r
+ * Whatever:hover is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\r
+ * Lesser General Public License for more details.\r
+ *\r
+ * Credits and thanks to:\r
+ * Arnoud Berendsen, Martin Reurings, Robert Hanson\r
+ *\r
+ * howto: body { behavior:url("csshover.htc"); }\r
+ * ------------------------------------------------------------\r
+ */\r
+\r
+var csshoverReg = /(^|\s)(([^a]([^ ]+)?)|(a([^#.][^ ]+)+)):(hover|active)/i,\r
+currentSheet, doc = window.document, hoverEvents = [], activators = {\r
+ onhover:{on:'onmouseover', off:'onmouseout'},\r
+ onactive:{on:'onmousedown', off:'onmouseup'}\r
+}\r
+\r
+function parseStylesheets() {\r
+ if(!/MSIE (5|6)/.test(navigator.userAgent)) return;\r
+ window.attachEvent('onunload', unhookHoverEvents);\r
+ var sheets = doc.styleSheets, l = sheets.length;\r
+ for(var i=0; i<l; i++) \r
+ parseStylesheet(sheets[i]);\r
+}\r
+ function parseStylesheet(sheet) {\r
+ if(sheet.imports) {\r
+ try {\r
+ var imports = sheet.imports, l = imports.length;\r
+ for(var i=0; i<l; i++) parseStylesheet(sheet.imports[i]);\r
+ } catch(securityException){}\r
+ }\r
+\r
+ try {\r
+ var rules = (currentSheet = sheet).rules, l = rules.length;\r
+ for(var j=0; j<l; j++) parseCSSRule(rules[j]);\r
+ } catch(securityException){}\r
+ }\r
+\r
+ function parseCSSRule(rule) {\r
+ var select = rule.selectorText, style = rule.style.cssText;\r
+ if(!csshoverReg.test(select) || !style) return;\r
+\r
+ var pseudo = select.replace(/[^:]+:([a-z-]+).*/i, 'on$1');\r
+ var newSelect = select.replace(/(\.([a-z0-9_-]+):[a-z]+)|(:[a-z]+)/gi, '.$2' + pseudo);\r
+ var className = (/\.([a-z0-9_-]*on(hover|active))/i).exec(newSelect)[1];\r
+ var affected = select.replace(/:(hover|active).*$/, '');\r
+ var elements = getElementsBySelect(affected);\r
+ if(elements.length == 0) return;\r
+\r
+ currentSheet.addRule(newSelect, style);\r
+ for(var i=0; i<elements.length; i++)\r
+ new HoverElement(elements[i], className, activators[pseudo]);\r
+ }\r
+\r
+function HoverElement(node, className, events) {\r
+ if(!node.hovers) node.hovers = {};\r
+ if(node.hovers[className]) return;\r
+ node.hovers[className] = true;\r
+ hookHoverEvent(node, events.on, function() { node.className += ' ' + className; });\r
+ hookHoverEvent(node, events.off, function() { node.className = node.className.replace(new RegExp('\\s+'+className, 'g'),''); });\r
+}\r
+ function hookHoverEvent(node, type, handler) {\r
+ node.attachEvent(type, handler);\r
+ hoverEvents[hoverEvents.length] = { \r
+ node:node, type:type, handler:handler \r
+ };\r
+ }\r
+\r
+ function unhookHoverEvents() {\r
+ for(var e,i=0; i<hoverEvents.length; i++) {\r
+ e = hoverEvents[i]; \r
+ e.node.detachEvent(e.type, e.handler);\r
+ }\r
+ }\r
+\r
+function getElementsBySelect(rule) {\r
+ var parts, nodes = [doc];\r
+ parts = rule.split(' ');\r
+ for(var i=0; i<parts.length; i++) {\r
+ nodes = getSelectedNodes(parts[i], nodes);\r
+ } return nodes;\r
+}\r
+ function getSelectedNodes(select, elements) {\r
+ var result, node, nodes = [];\r
+ var identify = (/\#([a-z0-9_-]+)/i).exec(select);\r
+ if(identify) {\r
+ var element = doc.getElementById(identify[1]);\r
+ return element? [element]:nodes;\r
+ }\r
+ \r
+ var classname = (/\.([a-z0-9_-]+)/i).exec(select);\r
+ var tagName = select.replace(/(\.|\#|\:)[a-z0-9_-]+/i, '');\r
+ var classReg = classname? new RegExp('\\b' + classname[1] + '\\b'):false;\r
+ for(var i=0; i<elements.length; i++) {\r
+ result = tagName? elements[i].all.tags(tagName):elements[i].all; \r
+ for(var j=0; j<result.length; j++) {\r
+ node = result[j];\r
+ if(classReg && !classReg.test(node.className)) continue;\r
+ nodes[nodes.length] = node;\r
+ }\r
+ } \r
+ \r
+ return nodes;\r
+ }\r
+\r
+window.parseStylesheets = parseStylesheets;\r
+</script>
\ No newline at end of file
--- /dev/null
+.jstEditor {
+ padding-left: 0px;
+}
+.jstEditor textarea, .jstEditor iframe {
+ margin: 0;
+}
+
+.jstHandle {
+ height: 10px;
+ font-size: 0.1em;
+ cursor: s-resize;
+ /*background: transparent url(img/resizer.png) no-repeat 45% 50%;*/
+}
+
+.jstElements {
+ padding: 3px 3px;
+}
+
+.jstElements button {
+ margin-right : 6px;
+ width : 24px;
+ height: 24px;
+ padding: 4px;
+ border-style: solid;
+ border-width: 1px;
+ border-color: #ddd;
+ background-color : #f7f7f7;
+ background-position : 50% 50%;
+ background-repeat: no-repeat;
+}
+.jstElements button:hover {
+ border-color : #000;
+}
+.jstElements button span {
+ display : none;
+}
+.jstElements span {
+ display : inline;
+}
+
+.jstSpacer {
+ width : 0px;
+ font-size: 1px;
+ margin-right: 4px;
+}
+
+.jstElements .help { float: right; margin-right: 1em; padding-top: 8px; font-size: 0.9em; }
+
+/* Buttons
+-------------------------------------------------------- */
+.jstb_strong {
+ background-image: url(../images/jstoolbar/bt_strong.png);
+}
+.jstb_em {
+ background-image: url(../images/jstoolbar/bt_em.png);
+}
+.jstb_ins {
+ background-image: url(../images/jstoolbar/bt_ins.png);
+}
+.jstb_del {
+ background-image: url(../images/jstoolbar/bt_del.png);
+}
+.jstb_code {
+ background-image: url(../images/jstoolbar/bt_code.png);
+}
+.jstb_h1 {
+ background-image: url(../images/jstoolbar/bt_h1.png);
+}
+.jstb_h2 {
+ background-image: url(../images/jstoolbar/bt_h2.png);
+}
+.jstb_h3 {
+ background-image: url(../images/jstoolbar/bt_h3.png);
+}
+.jstb_ul {
+ background-image: url(../images/jstoolbar/bt_ul.png);
+}
+.jstb_ol {
+ background-image: url(../images/jstoolbar/bt_ol.png);
+}
+.jstb_bq {
+ background-image: url(../images/jstoolbar/bt_bq.png);
+}
+.jstb_unbq {
+ background-image: url(../images/jstoolbar/bt_bq_remove.png);
+}
+.jstb_pre {
+ background-image: url(../images/jstoolbar/bt_pre.png);
+}
+.jstb_link {
+ background-image: url(../images/jstoolbar/bt_link.png);
+}
+.jstb_img {
+ background-image: url(../images/jstoolbar/bt_img.png);
+}
--- /dev/null
+
+div.changeset-changes ul { margin: 0; padding: 0; }
+div.changeset-changes ul > ul { margin-left: 18px; padding: 0; }
+
+li.change {
+ list-style-type:none;
+ background-image: url(../images/bullet_black.png);
+ background-position: 1px 1px;
+ background-repeat: no-repeat;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ padding-left: 20px;
+ margin: 0;
+}
+li.change.folder { background-image: url(../images/folder_open.png); }
+li.change.folder.change-A { background-image: url(../images/folder_open_add.png); }
+li.change.folder.change-M { background-image: url(../images/folder_open_orange.png); }
+li.change.change-A { background-image: url(../images/bullet_add.png); }
+li.change.change-M { background-image: url(../images/bullet_orange.png); }
+li.change.change-C { background-image: url(../images/bullet_blue.png); }
+li.change.change-R { background-image: url(../images/bullet_purple.png); }
+li.change.change-D { background-image: url(../images/bullet_delete.png); }
+
+li.change .copied-from { font-style: italic; color: #999; font-size: 0.9em; }
+li.change .copied-from:before { content: " - "}
+
+#changes-legend { float: right; font-size: 0.8em; margin: 0; }
+#changes-legend li { float: left; background-position: 5px 0; }
+
+table.filecontent { border: 1px solid #ccc; border-collapse: collapse; width:98%; }
+table.filecontent th { border: 1px solid #ccc; background-color: #eee; }
+table.filecontent th.filename { background-color: #e4e4d4; text-align: left; padding: 0.2em;}
+table.filecontent tr.spacing th { text-align:center; }
+table.filecontent tr.spacing td { height: 0.4em; background: #EAF2F5;}
+table.filecontent th.line-num {
+ border: 1px solid #d7d7d7;
+ font-size: 0.8em;
+ text-align: right;
+ width: 2%;
+ padding-right: 3px;
+ color: #999;
+}
+table.filecontent th.line-num a {
+ text-decoration: none;
+ color: inherit;
+}
+table.filecontent td.line-code pre {
+ white-space: pre-wrap; /* CSS2.1 compliant */
+ white-space: -moz-pre-wrap; /* Mozilla-based browsers */
+ white-space: -o-pre-wrap; /* Opera 7+ */
+}
+
+/* 12 different colors for the annonate view */
+table.annotate tr.bloc-0 {background: #FFFFBF;}
+table.annotate tr.bloc-1 {background: #EABFFF;}
+table.annotate tr.bloc-2 {background: #BFFFFF;}
+table.annotate tr.bloc-3 {background: #FFD9BF;}
+table.annotate tr.bloc-4 {background: #E6FFBF;}
+table.annotate tr.bloc-5 {background: #BFCFFF;}
+table.annotate tr.bloc-6 {background: #FFBFEF;}
+table.annotate tr.bloc-7 {background: #FFE6BF;}
+table.annotate tr.bloc-8 {background: #FFE680;}
+table.annotate tr.bloc-9 {background: #AA80FF;}
+table.annotate tr.bloc-10 {background: #FFBFDC;}
+table.annotate tr.bloc-11 {background: #BFE4FF;}
+
+table.annotate td.revision {
+ text-align: center;
+ width: 2%;
+ padding-left: 1em;
+ background: inherit;
+}
+
+table.annotate td.author {
+ text-align: center;
+ border-right: 1px solid #d7d7d7;
+ white-space: nowrap;
+ padding-left: 1em;
+ padding-right: 1em;
+ width: 3%;
+ background: inherit;
+ font-size: 90%;
+}
+
+table.annotate td.line-code { background-color: #fafafa; }
+
+div.action_M { background: #fd8 }
+div.action_D { background: #f88 }
+div.action_A { background: #bfb }
+
+/************* Coderay styles *************/
+
+table.CodeRay {
+ background-color: #fafafa;
+}
+.CodeRay pre { margin: 0px }
+
+span.CodeRay { white-space: pre; border: 0px; padding: 2px }
+
+.CodeRay .no { padding: 0px 4px }
+.CodeRay .code { }
+
+ol.CodeRay { font-size: 10pt }
+ol.CodeRay li { white-space: pre }
+
+.CodeRay .code pre { overflow: auto }
+
+.CodeRay .debug { color:white ! important; background:blue ! important; }
+
+.CodeRay .af { color:#00C }
+.CodeRay .an { color:#007 }
+.CodeRay .av { color:#700 }
+.CodeRay .aw { color:#C00 }
+.CodeRay .bi { color:#509; font-weight:bold }
+.CodeRay .c { color:#666; }
+
+.CodeRay .ch { color:#04D }
+.CodeRay .ch .k { color:#04D }
+.CodeRay .ch .dl { color:#039 }
+
+.CodeRay .cl { color:#B06; font-weight:bold }
+.CodeRay .co { color:#036; font-weight:bold }
+.CodeRay .cr { color:#0A0 }
+.CodeRay .cv { color:#369 }
+.CodeRay .df { color:#099; font-weight:bold }
+.CodeRay .di { color:#088; font-weight:bold }
+.CodeRay .dl { color:black }
+.CodeRay .do { color:#970 }
+.CodeRay .ds { color:#D42; font-weight:bold }
+.CodeRay .e { color:#666; font-weight:bold }
+.CodeRay .en { color:#800; font-weight:bold }
+.CodeRay .er { color:#F00; background-color:#FAA }
+.CodeRay .ex { color:#F00; font-weight:bold }
+.CodeRay .fl { color:#60E; font-weight:bold }
+.CodeRay .fu { color:#06B; font-weight:bold }
+.CodeRay .gv { color:#d70; font-weight:bold }
+.CodeRay .hx { color:#058; font-weight:bold }
+.CodeRay .i { color:#00D; font-weight:bold }
+.CodeRay .ic { color:#B44; font-weight:bold }
+
+.CodeRay .il { background: #eee }
+.CodeRay .il .il { background: #ddd }
+.CodeRay .il .il .il { background: #ccc }
+.CodeRay .il .idl { font-weight: bold; color: #888 }
+
+.CodeRay .in { color:#B2B; font-weight:bold }
+.CodeRay .iv { color:#33B }
+.CodeRay .la { color:#970; font-weight:bold }
+.CodeRay .lv { color:#963 }
+.CodeRay .oc { color:#40E; font-weight:bold }
+.CodeRay .of { color:#000; font-weight:bold }
+.CodeRay .op { }
+.CodeRay .pc { color:#038; font-weight:bold }
+.CodeRay .pd { color:#369; font-weight:bold }
+.CodeRay .pp { color:#579 }
+.CodeRay .pt { color:#339; font-weight:bold }
+.CodeRay .r { color:#080; font-weight:bold }
+
+.CodeRay .rx { background-color:#fff0ff }
+.CodeRay .rx .k { color:#808 }
+.CodeRay .rx .dl { color:#404 }
+.CodeRay .rx .mod { color:#C2C }
+.CodeRay .rx .fu { color:#404; font-weight: bold }
+
+.CodeRay .s { background-color:#fff0f0 }
+.CodeRay .s .s { background-color:#ffe0e0 }
+.CodeRay .s .s .s { background-color:#ffd0d0 }
+.CodeRay .s .k { color:#D20 }
+.CodeRay .s .dl { color:#710 }
+
+.CodeRay .sh { background-color:#f0fff0 }
+.CodeRay .sh .k { color:#2B2 }
+.CodeRay .sh .dl { color:#161 }
+
+.CodeRay .sy { color:#A60 }
+.CodeRay .sy .k { color:#A60 }
+.CodeRay .sy .dl { color:#630 }
+
+.CodeRay .ta { color:#070 }
+.CodeRay .tf { color:#070; font-weight:bold }
+.CodeRay .ts { color:#D70; font-weight:bold }
+.CodeRay .ty { color:#339; font-weight:bold }
+.CodeRay .v { color:#036 }
+.CodeRay .xt { color:#444 }
--- /dev/null
+Put your Redmine themes here.
--- /dev/null
+@import url(../../../stylesheets/application.css);
+
+body, #wrapper { background-color:#EEEEEE; }
+#header, #top-menu { margin: 0px 10px 0px 11px; }
+#main { background: #EEEEEE; margin: 8px 10px 0px 10px; }
+#content, #main.nosidebar #content { background: #fff; border-right: 1px solid #bbb; border-bottom: 1px solid #bbb; border-left: 1px solid #d7d7d7; border-top: 1px solid #d7d7d7; }
+#footer { background-color:#EEEEEE; border: 0px; }
+
+/* Headers */
+h2, h3, h4, .wiki h1, .wiki h2, .wiki h3 {border-bottom: 0px;}
+
+/* Menu */
+#main-menu li a { background-color: #507AAA; font-weight: bold;}
+#main-menu li a:hover { background: #507AAA; text-decoration: underline; }
+#main-menu li a.selected, #main-menu li a.selected:hover { background-color:#EEEEEE; }
+
+/* Tables */
+table.list tbody td, table.list tbody tr:hover td { border: solid 1px #d7d7d7; }
+table.list thead th {
+ border-width: 1px;
+ border-style: solid;
+ border-top-color: #d7d7d7;
+ border-right-color: #d7d7d7;
+ border-left-color: #d7d7d7;
+ border-bottom-color: #999999;
+}
+
+/* Issues grid styles by priorities (provided by Wynn Netherland) */
+table.list tr.issue a { color: #666; }
+
+tr.odd.priority-5, table.list tbody tr.odd.priority-5:hover { color: #900; font-weight: bold; }
+tr.odd.priority-5 { background: #ffc4c4; }
+tr.even.priority-5, table.list tbody tr.even.priority-5:hover { color: #900; font-weight: bold; }
+tr.even.priority-5 { background: #ffd4d4; }
+tr.priority-5 a, tr.priority-5:hover a { color: #900; }
+tr.odd.priority-5 td, tr.even.priority-5 td { border-color: #ffb4b4; }
+
+tr.odd.priority-4, table.list tbody tr.odd.priority-4:hover { color: #900; }
+tr.odd.priority-4 { background: #ffc4c4; }
+tr.even.priority-4, table.list tbody tr.even.priority-4:hover { color: #900; }
+tr.even.priority-4 { background: #ffd4d4; }
+tr.priority-4 a { color: #900; }
+tr.odd.priority-4 td, tr.even.priority-4 td { border-color: #ffb4b4; }
+
+tr.odd.priority-3, table.list tbody tr.odd.priority-3:hover { color: #900; }
+tr.odd.priority-3 { background: #fee; }
+tr.even.priority-3, table.list tbody tr.even.priority-3:hover { color: #900; }
+tr.even.priority-3 { background: #fff2f2; }
+tr.priority-3 a { color: #900; }
+tr.odd.priority-3 td, tr.even.priority-3 td { border-color: #fcc; }
+
+tr.odd.priority-1, table.list tbody tr.odd.priority-1:hover { color: #559; }
+tr.odd.priority-1 { background: #eaf7ff; }
+tr.even.priority-1, table.list tbody tr.even.priority-1:hover { color: #559; }
+tr.even.priority-1 { background: #f2faff; }
+tr.priority-1 a { color: #559; }
+tr.odd.priority-1 td, tr.even.priority-1 td { border-color: #add7f3; }
+
+/* Buttons */
+input[type="button"], input[type="submit"], input[type="reset"] { background-color: #f2f2f2; color: #222222; border: 1px outset #cccccc; }
+input[type="button"]:hover, input[type="submit"]:hover, input[type="reset"]:hover { background-color: #ccccbb; }
+
+/* Fields */
+input[type="text"], input[type="password"], textarea, select { padding: 2px; border: 1px solid #d7d7d7; }
+input[type="text"], input[type="password"] { padding: 3px; }
+input[type="text"]:focus, input[type="password"]:focus, textarea:focus, select:focus { border: 1px solid #888866; }
+option { border-bottom: 1px dotted #d7d7d7; }
+
+/* Misc */
+.box { background-color: #fcfcfc; }
--- /dev/null
+@import url(../../../stylesheets/application.css);
+
+body{ color:#303030; background:#e8eaec; }
+
+#top-menu { font-size: 80%; height: 2em; padding-top: 0.5em; background-color: #578bb8; }
+#top-menu a { font-weight: bold; }
+#header { background: #467aa7; height:5.8em; padding: 10px 0 0 0; }
+#header h1 { margin-left: 6px; }
+#quick-search { margin-right: 6px; }
+#main-menu { background-color: #578bb8; left: 0; border-top: 1px solid #fff; width: 100%; }
+#main-menu li { margin: 0; padding: 0; }
+#main-menu li a { background-color: #578bb8; border-right: 1px solid #fff; font-size: 90%; padding: 4px 8px 4px 8px; font-weight: bold; }
+#main-menu li a:hover { background-color: #80b0da; color: #ffffff; }
+#main-menu li a.selected, #main-menu li a.selected:hover { background-color: #80b0da; color: #ffffff; }
+
+#footer { background-color: #578bb8; border: 0; color: #fff;}
+#footer a { color: #fff; font-weight: bold; }
+
+#main { font:90% Verdana,Tahoma,Arial,sans-serif; background: #e8eaec; }
+#main a { font-weight: bold; color: #467aa7;}
+#main a:hover { color: #2a5a8a; text-decoration: underline; }
+#content { background: #fff; }
+#content .tabs ul { bottom:-1px; }
+
+h2, h3, h4, .wiki h1, .wiki h2, .wiki h3 { border-bottom: 0px; color:#606060; font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
+h2, .wiki h1 { letter-spacing:-1px; }
+h4 { border-bottom: dotted 1px #c0c0c0; }
+
+#top-menu a.home, #top-menu a.my-page, #top-menu a.projects, #top-menu a.administration, #top-menu a.help {
+ background-position: 0% 40%;
+ background-repeat: no-repeat;
+ padding-left: 20px;
+ padding-top: 2px;
+ padding-bottom: 3px;
+}
+
+#top-menu a.home { background-image: url(../../../images/home.png); }
+#top-menu a.my-page { background-image: url(../../../images/user_page.png); }
+#top-menu a.projects { background-image: url(../../../images/projects.png); }
+#top-menu a.administration { background-image: url(../../../images/admin.png); }
+#top-menu a.help { background-image: url(../../../images/help.png); }
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+$LOAD_PATH.unshift "#{RAILTIES_PATH}/builtin/rails_info"
+require 'commands/about'
+
+Redmine::About.print_plugin_info
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/breakpointer'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/console'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/dbconsole'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/destroy'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/generate'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/benchmarker'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/profiler'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/request'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/plugin'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/inspector'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/reaper'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/spawner'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/spinner'
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/runner'
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/server'
\ No newline at end of file
--- /dev/null
+class CustomField < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+ generator_for :field_format => 'string'
+
+ def self.next_name
+ @last_name ||= 'CustomField0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class CustomValue < ActiveRecord::Base
+end
--- /dev/null
+class Enumeration < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+ generator_for :type => 'TimeEntryActivity'
+
+ def self.next_name
+ @last_name ||= 'Enumeration0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class Issue < ActiveRecord::Base
+ generator_for :subject, :method => :next_subject
+ generator_for :author, :method => :next_author
+
+ def self.next_subject
+ @last_subject ||= 'Subject 0'
+ @last_subject.succ!
+ @last_subject
+ end
+
+ def self.next_author
+ User.generate_with_protected!
+ end
+
+end
--- /dev/null
+class IssueStatus < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+
+ def self.next_name
+ @last_name ||= 'Status 0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class Member < ActiveRecord::Base
+end
--- /dev/null
+class Project < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+ generator_for :identifier, :method => :next_identifier_from_object_daddy
+ generator_for :enabled_modules, :method => :all_modules
+ generator_for :trackers, :method => :next_tracker
+
+ def self.next_name
+ @last_name ||= 'Project 0'
+ @last_name.succ!
+ @last_name
+ end
+
+ # Project#next_identifier is defined on Redmine
+ def self.next_identifier_from_object_daddy
+ @last_identifier ||= 'project0'
+ @last_identifier.succ!
+ @last_identifier
+ end
+
+ def self.all_modules
+ returning [] do |modules|
+ Redmine::AccessControl.available_project_modules.each do |name|
+ modules << EnabledModule.new(:name => name.to_s)
+ end
+ end
+ end
+
+ def self.next_tracker
+ [Tracker.generate!]
+ end
+end
--- /dev/null
+class Query < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+
+ def self.next_name
+ @last_name ||= 'Query 0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class Role < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+
+ def self.next_name
+ @last_name ||= 'Role0'
+ @last_name.succ!
+ end
+end
--- /dev/null
+class TimeEntryActivity < Enumeration
+ generator_for :name, :method => :next_name
+ generator_for :type => 'TimeEntryActivity'
+
+ def self.next_name
+ @last_name ||= 'TimeEntryActivity0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class TimeEntry < ActiveRecord::Base
+ generator_for(:spent_on) { Date.today }
+ generator_for(:hours) { (rand * 10).round(2) } # 0.01 to 9.99
+
+end
--- /dev/null
+class Tracker < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+
+ def self.next_name
+ @last_name ||= 'Tracker 0'
+ @last_name.succ!
+ @last_name
+ end
+end
--- /dev/null
+class User < Principal
+ generator_for :login, :method => :next_login
+ generator_for :mail, :method => :next_email
+ generator_for :firstname, :method => :next_firstname
+ generator_for :lastname, :method => :next_lastname
+
+ def self.next_login
+ @gen_login ||= 'user1'
+ @gen_login.succ!
+ @gen_login
+ end
+
+ def self.next_email
+ @last_email ||= 'user1'
+ @last_email.succ!
+ "#{@last_email}@example.com"
+ end
+
+ def self.next_firstname
+ @last_firstname ||= 'Bob'
+ @last_firstname.succ!
+ @last_firstname
+ end
+
+ def self.next_lastname
+ @last_lastname ||= 'Doe'
+ @last_lastname.succ!
+ @last_lastname
+ end
+end
--- /dev/null
+class Version < ActiveRecord::Base
+ generator_for :name, :method => :next_name
+
+ def self.next_name
+ @last_name ||= 'Version 1.0.0'
+ @last_name.succ!
+ @last_name
+ end
+
+end
--- /dev/null
+---
+attachments_001:
+ created_on: 2006-07-19 21:07:27 +02:00
+ downloads: 0
+ content_type: text/plain
+ disk_filename: 060719210727_error281.txt
+ container_id: 3
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 1
+ container_type: Issue
+ filesize: 28
+ filename: error281.txt
+ author_id: 2
+attachments_002:
+ created_on: 2006-07-19 21:07:27 +02:00
+ downloads: 0
+ content_type: text/plain
+ disk_filename: 060719210727_document.txt
+ container_id: 1
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 2
+ container_type: Document
+ filesize: 28
+ filename: document.txt
+ author_id: 2
+attachments_003:
+ created_on: 2006-07-19 21:07:27 +02:00
+ downloads: 0
+ content_type: image/gif
+ disk_filename: 060719210727_logo.gif
+ container_id: 4
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 3
+ container_type: WikiPage
+ filesize: 280
+ filename: logo.gif
+ description: This is a logo
+ author_id: 2
+attachments_004:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_source.rb
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 4
+ filesize: 153
+ filename: source.rb
+ author_id: 2
+ description: This is a Ruby source file
+ content_type: application/x-ruby
+attachments_005:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_changeset.diff
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 5
+ filesize: 687
+ filename: changeset.diff
+ author_id: 2
+ content_type: text/x-diff
+attachments_006:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 6
+ filesize: 157
+ filename: archive.zip
+ author_id: 2
+ content_type: application/octet-stream
+attachments_007:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 4
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 7
+ filesize: 157
+ filename: archive.zip
+ author_id: 1
+ content_type: application/octet-stream
+attachments_008:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Project
+ container_id: 1
+ downloads: 0
+ disk_filename: 060719210727_project_file.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 8
+ filesize: 320
+ filename: project_file.zip
+ author_id: 2
+ content_type: application/octet-stream
+attachments_009:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Version
+ container_id: 1
+ downloads: 0
+ disk_filename: 060719210727_version_file.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 9
+ filesize: 452
+ filename: version_file.zip
+ author_id: 2
+ content_type: application/octet-stream
+attachments_010:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 2
+ downloads: 0
+ disk_filename: 060719210727_picture.jpg
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 10
+ filesize: 452
+ filename: picture.jpg
+ author_id: 2
+ content_type: image/jpeg
+
\ No newline at end of file
--- /dev/null
+---
+boards_001:
+ name: Help
+ project_id: 1
+ topics_count: 2
+ id: 1
+ description: Help board
+ position: 1
+ last_message_id: 6
+ messages_count: 6
+boards_002:
+ name: Discussion
+ project_id: 1
+ topics_count: 0
+ id: 2
+ description: Discussion board
+ position: 2
+ last_message_id:
+ messages_count: 0
+boards_003:
+ name: Discussion
+ project_id: 2
+ topics_count: 0
+ id: 3
+ description: Discussion board
+ position: 1
+ last_message_id:
+ messages_count: 0
--- /dev/null
+---
+changes_001:
+ id: 1
+ changeset_id: 100
+ action: A
+ path: /test/some/path/in/the/repo
+ from_path:
+ from_revision:
+changes_002:
+ id: 2
+ changeset_id: 100
+ action: A
+ path: /test/some/path/elsewhere/in/the/repo
+ from_path:
+ from_revision:
+changes_003:
+ id: 3
+ changeset_id: 101
+ action: M
+ path: /test/some/path/in/the/repo
+ from_path:
+ from_revision:
+
\ No newline at end of file
--- /dev/null
+---
+changesets_001:
+ commit_date: 2007-04-11
+ committed_on: 2007-04-11 15:14:44 +02:00
+ revision: 1
+ id: 100
+ comments: My very first commit
+ repository_id: 10
+ committer: dlopper
+ user_id: 3
+changesets_002:
+ commit_date: 2007-04-12
+ committed_on: 2007-04-12 15:14:44 +02:00
+ revision: 2
+ id: 101
+ comments: 'This commit fixes #1, #2 and references #1 & #3'
+ repository_id: 10
+ committer: dlopper
+ user_id: 3
+changesets_003:
+ commit_date: 2007-04-12
+ committed_on: 2007-04-12 15:14:44 +02:00
+ revision: 3
+ id: 102
+ comments: |-
+ A commit with wrong issue ids
+ IssueID 666 3
+ repository_id: 10
+ committer: dlopper
+ user_id: 3
+changesets_004:
+ commit_date: 2007-04-12
+ committed_on: 2007-04-12 15:14:44 +02:00
+ revision: 4
+ id: 103
+ comments: |-
+ A commit with an issue id of an other project
+ IssueID 4 2
+ repository_id: 10
+ committer: dlopper
+ user_id: 3
+changesets_005:
+ commit_date: "2007-09-10"
+ comments: Modified one file in the folder.
+ committed_on: 2007-09-10 19:01:08
+ revision: "5"
+ id: 104
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+changesets_006:
+ commit_date: "2007-09-10"
+ comments: Moved helloworld.rb from / to /folder.
+ committed_on: 2007-09-10 19:01:47
+ revision: "6"
+ id: 105
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+changesets_007:
+ commit_date: "2007-09-10"
+ comments: Removed one file.
+ committed_on: 2007-09-10 19:02:16
+ revision: "7"
+ id: 106
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+changesets_008:
+ commit_date: "2007-09-10"
+ comments: |-
+ This commits references an issue.
+ Refs #2
+ committed_on: 2007-09-10 19:04:35
+ revision: "8"
+ id: 107
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+changesets_009:
+ commit_date: "2009-09-10"
+ comments: One file added.
+ committed_on: 2009-09-10 19:04:35
+ revision: "9"
+ id: 108
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+changesets_010:
+ commit_date: "2009-09-10"
+ comments: Same file modified.
+ committed_on: 2009-09-10 19:04:35
+ revision: "10"
+ id: 109
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
+
\ No newline at end of file
--- /dev/null
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+comments_001:
+ commented_type: News
+ commented_id: 1
+ id: 1
+ author_id: 1
+ comments: my first comment
+ created_on: 2006-12-10 18:10:10 +01:00
+ updated_on: 2006-12-10 18:10:10 +01:00
+comments_002:
+ commented_type: News
+ commented_id: 1
+ id: 2
+ author_id: 2
+ comments: This is an other comment
+ created_on: 2006-12-10 18:12:10 +01:00
+ updated_on: 2006-12-10 18:12:10 +01:00
+
\ No newline at end of file
--- /dev/null
+---
+custom_fields_001:
+ name: Database
+ min_length: 0
+ regexp: ""
+ is_for_all: true
+ is_filter: true
+ type: IssueCustomField
+ max_length: 0
+ possible_values:
+ - MySQL
+ - PostgreSQL
+ - Oracle
+ id: 1
+ is_required: false
+ field_format: list
+ default_value: ""
+ editable: true
+custom_fields_002:
+ name: Searchable field
+ min_length: 1
+ regexp: ""
+ is_for_all: true
+ type: IssueCustomField
+ max_length: 100
+ possible_values: ""
+ id: 2
+ is_required: false
+ field_format: string
+ searchable: true
+ default_value: "Default string"
+ editable: true
+custom_fields_003:
+ name: Development status
+ min_length: 0
+ regexp: ""
+ is_for_all: false
+ is_filter: true
+ type: ProjectCustomField
+ max_length: 0
+ possible_values:
+ - Stable
+ - Beta
+ - Alpha
+ - Planning
+ id: 3
+ is_required: true
+ field_format: list
+ default_value: ""
+ editable: true
+custom_fields_004:
+ name: Phone number
+ min_length: 0
+ regexp: ""
+ is_for_all: false
+ type: UserCustomField
+ max_length: 0
+ possible_values: ""
+ id: 4
+ is_required: false
+ field_format: string
+ default_value: ""
+ editable: true
+custom_fields_005:
+ name: Money
+ min_length: 0
+ regexp: ""
+ is_for_all: false
+ type: UserCustomField
+ max_length: 0
+ possible_values: ""
+ id: 5
+ is_required: false
+ field_format: float
+ default_value: ""
+ editable: true
+custom_fields_006:
+ name: Float field
+ min_length: 0
+ regexp: ""
+ is_for_all: true
+ type: IssueCustomField
+ max_length: 0
+ possible_values: ""
+ id: 6
+ is_required: false
+ field_format: float
+ default_value: ""
+ editable: true
+custom_fields_007:
+ name: Billable
+ min_length: 0
+ regexp: ""
+ is_for_all: false
+ is_filter: true
+ type: TimeEntryActivityCustomField
+ max_length: 0
+ possible_values: ""
+ id: 7
+ is_required: false
+ field_format: bool
+ default_value: ""
+ editable: true
--- /dev/null
+---
+custom_fields_trackers_001:
+ custom_field_id: 1
+ tracker_id: 1
+custom_fields_trackers_002:
+ custom_field_id: 2
+ tracker_id: 1
+custom_fields_trackers_003:
+ custom_field_id: 2
+ tracker_id: 3
+custom_fields_trackers_004:
+ custom_field_id: 6
+ tracker_id: 1
+custom_fields_trackers_005:
+ custom_field_id: 6
+ tracker_id: 2
+custom_fields_trackers_006:
+ custom_field_id: 6
+ tracker_id: 3
--- /dev/null
+---
+custom_values_006:
+ customized_type: Issue
+ custom_field_id: 2
+ customized_id: 3
+ id: 6
+ value: "125"
+custom_values_007:
+ customized_type: Project
+ custom_field_id: 3
+ customized_id: 1
+ id: 7
+ value: Stable
+custom_values_001:
+ customized_type: Principal
+ custom_field_id: 4
+ customized_id: 3
+ id: 1
+ value: ""
+custom_values_002:
+ customized_type: Principal
+ custom_field_id: 4
+ customized_id: 4
+ id: 2
+ value: 01 23 45 67 89
+custom_values_003:
+ customized_type: Principal
+ custom_field_id: 4
+ customized_id: 2
+ id: 3
+ value: ""
+custom_values_004:
+ customized_type: Issue
+ custom_field_id: 2
+ customized_id: 1
+ id: 4
+ value: "125"
+custom_values_005:
+ customized_type: Issue
+ custom_field_id: 2
+ customized_id: 2
+ id: 5
+ value: ""
+custom_values_008:
+ customized_type: Issue
+ custom_field_id: 1
+ customized_id: 3
+ id: 8
+ value: "MySQL"
+custom_values_009:
+ customized_type: Issue
+ custom_field_id: 2
+ customized_id: 3
+ id: 9
+ value: "this is a stringforcustomfield search"
+custom_values_010:
+ customized_type: Issue
+ custom_field_id: 6
+ customized_id: 1
+ id: 10
+ value: "2.1"
+custom_values_011:
+ customized_type: Issue
+ custom_field_id: 6
+ customized_id: 2
+ id: 11
+ value: "2.05"
+custom_values_012:
+ customized_type: Issue
+ custom_field_id: 6
+ customized_id: 3
+ id: 12
+ value: "11.65"
+custom_values_013:
+ customized_type: Issue
+ custom_field_id: 6
+ customized_id: 7
+ id: 13
+ value: ""
+custom_values_014:
+ customized_type: Issue
+ custom_field_id: 6
+ customized_id: 5
+ id: 14
+ value: "-7.6"
+custom_values_015:
+ customized_type: Enumeration
+ custom_field_id: 7
+ customized_id: 10
+ id: 15
+ value: true
+custom_values_016:
+ customized_type: Enumeration
+ custom_field_id: 7
+ customized_id: 11
+ id: 16
+ value: '1'
--- /dev/null
+Index: app/views/settings/_general.rhtml
+===================================================================
+--- app/views/settings/_general.rhtml (revision 2094)
++++ app/views/settings/_general.rhtml (working copy)
+@@ -48,6 +48,9 @@
+ <p><label><%= l(:setting_feeds_limit) %></label>
+ <%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %></p>
+
++<p><label><%= l(:setting_diff_max_lines_displayed) %></label>
++<%= text_field_tag 'settings[diff_max_lines_displayed]', Setting.diff_max_lines_displayed, :size => 6 %></p>
++
+ <p><label><%= l(:setting_gravatar_enabled) %></label>
+ <%= check_box_tag 'settings[gravatar_enabled]', 1, Setting.gravatar_enabled? %><%= hidden_field_tag 'settings[gravatar_enabled]', 0 %></p>
+ </div>
+Index: app/views/common/_diff.rhtml
+===================================================================
+--- app/views/common/_diff.rhtml (revision 2111)
++++ app/views/common/_diff.rhtml (working copy)
+@@ -1,4 +1,5 @@
+-<% Redmine::UnifiedDiff.new(diff, :type => diff_type).each do |table_file| -%>
++<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%>
++<% diff.each do |table_file| -%>
+ <div class="autoscroll">
+ <% if diff_type == 'sbs' -%>
+ <table class="filecontent CodeRay">
+@@ -62,3 +63,5 @@
+
+ </div>
+ <% end -%>
++
++<%= l(:text_diff_truncated) if diff.truncated? %>
+Index: lang/lt.yml
+===================================================================
+--- config/settings.yml (revision 2094)
++++ config/settings.yml (working copy)
+@@ -61,6 +61,9 @@
+ feeds_limit:
+ format: int
+ default: 15
++diff_max_lines_displayed:
++ format: int
++ default: 1500
+ enabled_scm:
+ serialized: true
+ default:
+Index: lib/redmine/unified_diff.rb
+===================================================================
+--- lib/redmine/unified_diff.rb (revision 2110)
++++ lib/redmine/unified_diff.rb (working copy)
+@@ -19,8 +19,11 @@
+ # Class used to parse unified diffs
+ class UnifiedDiff < Array
+ def initialize(diff, options={})
++ options.assert_valid_keys(:type, :max_lines)
+ diff_type = options[:type] || 'inline'
+
++ lines = 0
++ @truncated = false
+ diff_table = DiffTable.new(diff_type)
+ diff.each do |line|
+ if line =~ /^(---|\+\+\+) (.*)$/
+@@ -28,10 +31,17 @@
+ diff_table = DiffTable.new(diff_type)
+ end
+ diff_table.add_line line
++ lines += 1
++ if options[:max_lines] && lines > options[:max_lines]
++ @truncated = true
++ break
++ end
+ end
+ self << diff_table unless diff_table.empty?
+ self
+ end
++
++ def truncated?; @truncated; end
+ end
+
+ # Class that represents a file diff
--- /dev/null
+documents_001:
+ created_on: 2007-01-27 15:08:27 +01:00
+ project_id: 1
+ title: "Test document"
+ id: 1
+ description: "Document description"
+ category_id: 1
\ No newline at end of file
--- /dev/null
+---
+enabled_modules_001:
+ name: issue_tracking
+ project_id: 1
+ id: 1
+enabled_modules_002:
+ name: time_tracking
+ project_id: 1
+ id: 2
+enabled_modules_003:
+ name: news
+ project_id: 1
+ id: 3
+enabled_modules_004:
+ name: documents
+ project_id: 1
+ id: 4
+enabled_modules_005:
+ name: files
+ project_id: 1
+ id: 5
+enabled_modules_006:
+ name: wiki
+ project_id: 1
+ id: 6
+enabled_modules_007:
+ name: repository
+ project_id: 1
+ id: 7
+enabled_modules_008:
+ name: boards
+ project_id: 1
+ id: 8
+enabled_modules_009:
+ name: repository
+ project_id: 3
+ id: 9
+enabled_modules_010:
+ name: wiki
+ project_id: 3
+ id: 10
+enabled_modules_011:
+ name: issue_tracking
+ project_id: 2
+ id: 11
+enabled_modules_012:
+ name: time_tracking
+ project_id: 3
+ id: 12
+enabled_modules_013:
+ name: issue_tracking
+ project_id: 3
+ id: 13
+enabled_modules_014:
+ name: issue_tracking
+ project_id: 5
+ id: 14
+enabled_modules_015:
+ name: wiki
+ project_id: 2
+ id: 15
--- /dev/null
+---
+enumerations_001:
+ name: Uncategorized
+ id: 1
+ opt: DCAT
+ type: DocumentCategory
+ active: true
+enumerations_002:
+ name: User documentation
+ id: 2
+ opt: DCAT
+ type: DocumentCategory
+ active: true
+enumerations_003:
+ name: Technical documentation
+ id: 3
+ opt: DCAT
+ type: DocumentCategory
+ active: true
+enumerations_004:
+ name: Low
+ id: 4
+ opt: IPRI
+ type: IssuePriority
+ active: true
+enumerations_005:
+ name: Normal
+ id: 5
+ opt: IPRI
+ type: IssuePriority
+ is_default: true
+ active: true
+enumerations_006:
+ name: High
+ id: 6
+ opt: IPRI
+ type: IssuePriority
+ active: true
+enumerations_007:
+ name: Urgent
+ id: 7
+ opt: IPRI
+ type: IssuePriority
+ active: true
+enumerations_008:
+ name: Immediate
+ id: 8
+ opt: IPRI
+ type: IssuePriority
+ active: true
+enumerations_009:
+ name: Design
+ id: 9
+ opt: ACTI
+ type: TimeEntryActivity
+ active: true
+enumerations_010:
+ name: Development
+ id: 10
+ opt: ACTI
+ type: TimeEntryActivity
+ is_default: true
+ active: true
+enumerations_011:
+ name: QA
+ id: 11
+ opt: ACTI
+ type: TimeEntryActivity
+ active: true
+enumerations_012:
+ name: Default Enumeration
+ id: 12
+ opt: ''
+ type: Enumeration
+ is_default: true
+ active: true
+enumerations_013:
+ name: Another Enumeration
+ id: 13
+ opt: ''
+ type: Enumeration
+ active: true
+enumerations_014:
+ name: Inactive Activity
+ id: 14
+ opt: ACTI
+ type: TimeEntryActivity
+ active: false
--- /dev/null
+Index: trunk/app/controllers/issues_controller.rb
+===================================================================
+--- trunk/app/controllers/issues_controller.rb (r\82vision 1483)
++++ trunk/app/controllers/issues_controller.rb (r\82vision 1484)
+@@ -149,7 +149,7 @@
+ attach_files(@issue, params[:attachments])
+ flash[:notice] = l(:notice_successful_create)
+ Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
+- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project
++ redirect_to :controller => 'issues', :action => 'show', :id => @issue
+ return
+ end
+ end
--- /dev/null
+# The Greeter class
+class Greeter
+ def initialize(name)
+ @name = name.capitalize
+ end
+
+ def salute
+ puts "Hello #{@name}!"
+ end
+end
--- /dev/null
+this is a text file for upload tests\r
+with multiple lines\r
--- /dev/null
+---
+groups_users_001:
+ group_id: 10
+ user_id: 8
+
\ No newline at end of file
--- /dev/null
+---
+issue_categories_001:
+ name: Printing
+ project_id: 1
+ assigned_to_id: 2
+ id: 1
+issue_categories_002:
+ name: Recipes
+ project_id: 1
+ assigned_to_id:
+ id: 2
+issue_categories_003:
+ name: Stock management
+ project_id: 2
+ assigned_to_id:
+ id: 3
+issue_categories_004:
+ name: Printing
+ project_id: 2
+ assigned_to_id:
+ id: 4
+
\ No newline at end of file
--- /dev/null
+issue_relation_001:
+ id: 1
+ issue_from_id: 10
+ issue_to_id: 9
+ relation_type: blocks
+ delay:
+issue_relation_002:
+ id: 2
+ issue_from_id: 2
+ issue_to_id: 3
+ relation_type: relates
+ delay:
+
--- /dev/null
+---
+issue_statuses_006:
+ name: Rejected
+ is_default: false
+ is_closed: true
+ id: 6
+issue_statuses_001:
+ name: New
+ is_default: true
+ is_closed: false
+ id: 1
+issue_statuses_002:
+ name: Assigned
+ is_default: false
+ is_closed: false
+ id: 2
+issue_statuses_003:
+ name: Resolved
+ is_default: false
+ is_closed: false
+ id: 3
+issue_statuses_004:
+ name: Feedback
+ is_default: false
+ is_closed: false
+ id: 4
+issue_statuses_005:
+ name: Closed
+ is_default: false
+ is_closed: true
+ id: 5
--- /dev/null
+---
+issues_001:
+ created_on: <%= 3.days.ago.to_date.to_s(:db) %>
+ project_id: 1
+ updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Can't print recipes
+ id: 1
+ fixed_version_id:
+ category_id: 1
+ description: Unable to print recipes
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= 1.day.ago.to_date.to_s(:db) %>
+ due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
+issues_002:
+ created_on: 2006-07-19 21:04:21 +02:00
+ project_id: 1
+ updated_on: 2006-07-19 21:09:50 +02:00
+ priority_id: 5
+ subject: Add ingredients categories
+ id: 2
+ fixed_version_id: 2
+ category_id:
+ description: Ingredients of the recipe should be classified by categories
+ tracker_id: 2
+ assigned_to_id: 3
+ author_id: 2
+ status_id: 2
+ start_date: <%= 2.day.ago.to_date.to_s(:db) %>
+ due_date:
+issues_003:
+ created_on: 2006-07-19 21:07:27 +02:00
+ project_id: 1
+ updated_on: 2006-07-19 21:07:27 +02:00
+ priority_id: 4
+ subject: Error 281 when updating a recipe
+ id: 3
+ fixed_version_id:
+ category_id:
+ description: Error 281 is encountered when saving a recipe
+ tracker_id: 1
+ assigned_to_id: 3
+ author_id: 2
+ status_id: 1
+ start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
+ due_date: <%= 40.day.ago.to_date.to_s(:db) %>
+issues_004:
+ created_on: <%= 5.days.ago.to_date.to_s(:db) %>
+ project_id: 2
+ updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Issue on project 2
+ id: 4
+ fixed_version_id:
+ category_id:
+ description: Issue on project 2
+ tracker_id: 1
+ assigned_to_id: 2
+ author_id: 2
+ status_id: 1
+issues_005:
+ created_on: <%= 5.days.ago.to_date.to_s(:db) %>
+ project_id: 3
+ updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Subproject issue
+ id: 5
+ fixed_version_id:
+ category_id:
+ description: This is an issue on a cookbook subproject
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+issues_006:
+ created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ project_id: 5
+ updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Issue of a private subproject
+ id: 6
+ fixed_version_id:
+ category_id:
+ description: This is an issue of a private subproject of cookbook
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= Date.today.to_s(:db) %>
+ due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+issues_007:
+ created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+ project_id: 1
+ updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Issue due today
+ id: 7
+ fixed_version_id:
+ category_id:
+ description: This is an issue that is due today
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= 10.days.ago.to_s(:db) %>
+ due_date: <%= Date.today.to_s(:db) %>
+ lock_version: 0
+issues_008:
+ created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+ project_id: 1
+ updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Closed issue
+ id: 8
+ fixed_version_id:
+ category_id:
+ description: This is a closed issue.
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 5
+ start_date:
+ due_date:
+ lock_version: 0
+issues_009:
+ created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ project_id: 5
+ updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Blocked Issue
+ id: 9
+ fixed_version_id:
+ category_id:
+ description: This is an issue that is blocked by issue #10
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= Date.today.to_s(:db) %>
+ due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+issues_010:
+ created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ project_id: 5
+ updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Issue Doing the Blocking
+ id: 10
+ fixed_version_id:
+ category_id:
+ description: This is an issue that blocks issue #9
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= Date.today.to_s(:db) %>
+ due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+issues_011:
+ created_on: <%= 3.days.ago.to_date.to_s(:db) %>
+ project_id: 1
+ updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Closed issue on a closed version
+ id: 11
+ fixed_version_id: 1
+ category_id: 1
+ description:
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 5
+ start_date: <%= 1.day.ago.to_date.to_s(:db) %>
+ due_date:
+issues_012:
+ created_on: <%= 3.days.ago.to_date.to_s(:db) %>
+ project_id: 1
+ updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
+ priority_id: 5
+ subject: Closed issue on a locked version
+ id: 12
+ fixed_version_id: 2
+ category_id: 1
+ description:
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 5
+ start_date: <%= 1.day.ago.to_date.to_s(:db) %>
+ due_date:
--- /dev/null
+---
+journal_details_001:
+ old_value: "1"
+ property: attr
+ id: 1
+ value: "2"
+ prop_key: status_id
+ journal_id: 1
+journal_details_002:
+ old_value: "40"
+ property: attr
+ id: 2
+ value: "30"
+ prop_key: done_ratio
+ journal_id: 1
--- /dev/null
+---
+journals_001:
+ created_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ notes: "Journal notes"
+ id: 1
+ journalized_type: Issue
+ user_id: 1
+ journalized_id: 1
+journals_002:
+ created_on: <%= 1.days.ago.to_date.to_s(:db) %>
+ notes: "Some notes with Redmine links: #2, r2."
+ id: 2
+ journalized_type: Issue
+ user_id: 2
+ journalized_id: 1
+journals_003:
+ created_on: <%= 1.days.ago.to_date.to_s(:db) %>
+ notes: "A comment with inline image: !picture.jpg!"
+ id: 3
+ journalized_type: Issue
+ user_id: 2
+ journalized_id: 2
+
\ No newline at end of file
--- /dev/null
+Message-ID: <4974C93E.3070005@somenet.foo>
+Date: Mon, 19 Jan 2009 19:41:02 +0100
+From: "John Smith" <jsmith@somenet.foo>
+User-Agent: Thunderbird 2.0.0.19 (Windows/20081209)
+MIME-Version: 1.0
+To: redmine@somenet.foo
+Subject: Reply via email
+References: <redmine.message-2.20070512171800@somenet.foo>
+In-Reply-To: <redmine.message-2.20070512171800@somenet.foo>
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a reply to a forum message.
+
+
--- /dev/null
+Message-ID: <4974C93E.3070005@somenet.foo>
+Date: Mon, 19 Jan 2009 19:41:02 +0100
+From: "John Smith" <jsmith@somenet.foo>
+User-Agent: Thunderbird 2.0.0.19 (Windows/20081209)
+MIME-Version: 1.0
+To: redmine@somenet.foo
+Subject: Re: [eCookbook - Help board - msg2] Reply to the first post
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+This is a reply to a forum message.
+
+
--- /dev/null
+Return-Path: <john.doe@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+ by OSIRIS\r
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200\r
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>\r
+From: "John Doe" <john.doe@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+Subject: Ticket by unknown user\r
+Date: Sun, 22 Jun 2008 12:28:07 +0200\r
+MIME-Version: 1.0\r
+Content-Type: text/plain;\r
+ format=flowed;\r
+ charset="iso-8859-1";\r
+ reply-type=original\r
+Content-Transfer-Encoding: 7bit\r
+\r
+This is a ticket submitted by an unknown user.\r
+\r
--- /dev/null
+Return-Path: <redmine@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+ by OSIRIS\r
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200\r
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>\r
+From: "John Doe" <Redmine@example.net>\r
+To: <redmine@somenet.foo>\r
+Subject: Ticket with the Redmine emission address\r
+Date: Sun, 22 Jun 2008 12:28:07 +0200\r
+MIME-Version: 1.0\r
+Content-Type: text/plain;\r
+ format=flowed;\r
+ charset="iso-8859-1";\r
+ reply-type=original\r
+Content-Transfer-Encoding: 7bit\r
+\r
+This is a ticket submitted with the Redmine emission address.\r
+It should be ignored.\r
+\r
--- /dev/null
+x-sender: <jsmith@somenet.foo>
+x-receiver: <redmine@somenet.foo>
+Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0);
+ Sun, 14 Dec 2008 16:18:06 GMT
+Message-ID: <494531B9.1070709@somenet.foo>
+Date: Sun, 14 Dec 2008 17:18:01 +0100
+From: "John Smith" <jsmith@somenet.foo>
+User-Agent: Thunderbird 2.0.0.18 (Windows/20081105)
+MIME-Version: 1.0
+To: redmine@somenet.foo
+Subject: HTML email
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+</head>
+<body bgcolor="#ffffff" text="#000000">
+This is a <b>html-only</b> email.<br>
+</body>
+</html>
--- /dev/null
+Return-Path: <JSmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+ by OSIRIS\r
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200\r
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>\r
+From: "John Smith" <JSmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+Subject: New ticket on a given project\r
+Date: Sun, 22 Jun 2008 12:28:07 +0200\r
+MIME-Version: 1.0\r
+Content-Type: text/plain;\r
+ format=flowed;\r
+ charset="iso-8859-1";\r
+ reply-type=original\r
+Content-Transfer-Encoding: 7bit\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet \r
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus \r
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti \r
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In \r
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras \r
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum \r
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus \r
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique \r
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et \r
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse \r
+platea dictumst.\r
+\r
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque \r
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. \r
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, \r
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, \r
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo \r
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.\r
+\r
+Project: onlinestore\r
+Status: Resolved\r
+\r
--- /dev/null
+Return-Path: <jsmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+ by OSIRIS\r
+ with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200\r
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>\r
+In-Reply-To: <redmine.issue-2.20060719210421@osiris>\r
+From: "John Smith" <jsmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>\r
+Subject: Re: Add ingredients categories\r
+Date: Sat, 21 Jun 2008 18:41:39 +0200\r
+MIME-Version: 1.0\r
+Content-Type: multipart/alternative;\r
+ boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+This is a multi-part message in MIME format.\r
+\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0\r
+Content-Type: text/plain;\r
+ charset="utf-8"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+This is reply\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0\r
+Content-Type: text/html;\r
+ charset="utf-8"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r
+<HTML><HEAD>\r
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">\r
+<STYLE>BODY {\r
+ FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif\r
+}\r
+BODY H1 {\r
+ FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =\r
+sans-serif\r
+}\r
+A {\r
+ COLOR: #2a5685\r
+}\r
+A:link {\r
+ COLOR: #2a5685\r
+}\r
+A:visited {\r
+ COLOR: #2a5685\r
+}\r
+A:hover {\r
+ COLOR: #c61a1a\r
+}\r
+A:active {\r
+ COLOR: #c61a1a\r
+}\r
+HR {\r
+ BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =\r
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px\r
+}\r
+.footer {\r
+ FONT-SIZE: 0.8em; FONT-STYLE: italic\r
+}\r
+</STYLE>\r
+\r
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>\r
+<BODY bgColor=3D#ffffff>\r
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =\r
+size=3D2>This is=20\r
+reply</FONT></DIV></SPAN></BODY></HTML>\r
+\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--\r
+\r
--- /dev/null
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>
+Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
+Date: Sat, 21 Jun 2008 18:41:39 +0200
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+This is reply
+
+Status: Resolved
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
+<STYLE>BODY {
+ FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
+}
+BODY H1 {
+ FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
+sans-serif
+}
+A {
+ COLOR: #2a5685
+}
+A:link {
+ COLOR: #2a5685
+}
+A:visited {
+ COLOR: #2a5685
+}
+A:hover {
+ COLOR: #c61a1a
+}
+A:active {
+ COLOR: #c61a1a
+}
+HR {
+ BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
+}
+.footer {
+ FONT-SIZE: 0.8em; FONT-STYLE: italic
+}
+</STYLE>
+
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
+size=3D2>This is=20
+reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML>
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--
+
--- /dev/null
+Return-Path: <jsmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+ by OSIRIS\r
+ with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200\r
+Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris>\r
+From: "John Smith" <jsmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+Subject: Ticket created by email with attachment\r
+Date: Sat, 21 Jun 2008 15:53:25 +0200\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed;\r
+ boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270"\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+This is a multi-part message in MIME format.\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270\r
+Content-Type: multipart/alternative;\r
+ boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270"\r
+\r
+\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270\r
+Content-Type: text/plain;\r
+ charset="iso-8859-1"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+This is a new ticket with attachments\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270\r
+Content-Type: text/html;\r
+ charset="iso-8859-1"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r
+<HTML><HEAD>\r
+<META http-equiv=3DContent-Type content=3D"text/html; =\r
+charset=3Diso-8859-1">\r
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR>\r
+<STYLE></STYLE>\r
+</HEAD>\r
+<BODY bgColor=3D#ffffff>\r
+<DIV><FONT face=3DArial size=3D2>This is a new ticket with=20\r
+attachments</FONT></DIV></BODY></HTML>\r
+\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270--\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270\r
+Content-Type: image/jpeg;\r
+ name="Paella.jpg"\r
+Content-Transfer-Encoding: base64\r
+Content-Disposition: attachment;\r
+ filename="Paella.jpg"\r
+\r
+/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU\r
+FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo\r
+KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCACmAMgDASIA\r
+AhEBAxEB/8QAHQAAAgMBAQEBAQAAAAAAAAAABQYABAcDCAIBCf/EADsQAAEDAwMCBQIDBQcFAQAA\r
+AAECAwQABREGEiExQQcTIlFhcYEUMpEVI0Kh0QhSYrHB4fAWJCUzQ3L/xAAaAQADAQEBAQAAAAAA\r
+AAAAAAADBAUCAQYA/8QAKhEAAgIBBAICAgIDAAMAAAAAAQIAAxEEEiExIkEFE1FhMnFCkaEjwdH/\r
+2gAMAwEAAhEDEQA/ACTUdSsdhRCNE54GTRaBaXHiBtNOVo0wEpSt8BKfmpWCZRPHcVbdZ3X1J9Jx\r
+Tla9OBpIU8Noo7Gjx4qdrCBkfxGupUSck13GJjeT1ObEdthOG04/zpX8SNXjR1njym46ZMmQ+llp\r
+pStuc9T9hRq/X22afhKl3iazEYHdxWCfgDqT9K83eKfiFG1RfIEi3tuC3W9KlNh0YLqyeuO3QV0D\r
+MznM9O2uai4QI8psYQ8gLA9virY615P034xX+zNNslLDsMKOG1J5HuAa3nQPiBZ9WtpUy4lmcE4U\r
+ypXP2rmMHmcI/EealD7te7ZZ2S7dLhGiN9cvOBP+dIF18btHw3C1DkSbi7nATGZJBPwTitTIyZp9\r
+SsCun9oJaEFUDTy0oyQFyXSOfoB/rQOL466huE9LIagxW1A48tkuKJxwBlQrm4YzNhGPE9Mmua8Y\r
+JrzsrXPiQ42y7+KtsZt4kpS8ltK0p91J5IzXGFr3xFef8pMqE4vJABZT6se3FDNyEZzNCh89Tfbv\r
+aoV2iKj3GO2+0eyh0+h7VkWq/CqTDUqXpp0uJHPkKOFj6HofvQRzxZ1bbwFTG7c+jO0lKeh+cGi8\r
+bxrebZZVMtjDqljKgw4Rt9uuea5vEIEceoL09ZnHQoyGy3KaOFhxO0j6g0J8QNPr3tzorHmsJSUv\r
+NgdQeprTIuqbfqdtD7MRxh7HO/H6ZHWlnW0e5tQnv2WgupAyEg8p9xUl7WGowpzKCoDXyJ5nvMdK\r
+Uuho4bSv057CqK2stIWrgEZp2kWtE+O5+MC0OKUchHFCbnaWVNeW1KU3tTtwtAUkj6jkfpXoK7gQ\r
+AZLsqYEmJ0mUBlLeCfeqHKl5PqJopNhriupQWyoqPpKeQfpTXYPDW+3ZlEhTTcVpXI8w+oj6Cmty\r
+qMxTazHAi1ZLG/PXuKClv3Ip7t2n4yI3lKZSsEc7hmicXwfu5ThN22fCUH+tXB4QX1KdzN6WVjth\r
+Q/1oDuG/yjCIV/xgWLouQFfiLK/5LqejbnKT9D1FStX05DRaYrTN8K232wEl1aMJV856VKF9hPc3\r
+9QPM32HEjxEjykBSh/ERSd4s61uGjLbBnQrcie2t4pfClEFKAM8Y704uvtsMrdfcQ20gZUtZAAHu\r
+SawHxt8V7PKt/wCytPp/aLrToW7JAPlNkAjAPfOfpQ0JY4E42B3Nf09ruwXvTQvjM9lmGkfvvOWE\r
+llXdKvn/ADrONZeNwU28zo2Ml1tHpXc5Y2spP+EHlR/5ivOzYkPPKdjMechRDjrCUHy1Ec9Aa1Lw\r
+l0VF10pcy4XJC0RlbTFTgKbHwnokfSibFXkzAJbiJ0tN81jc1yHXplzkEEqkPA7UjvtR2H1/SrOl\r
+rGu6NvP7Q8yhaWkDruVj/n616Lvl20n4Z2cpeS02tSfRHbAU69/t8nivOGoNXzNQSVRbFAbtsFal\r
+FESEjBOepUR1rBs3D8CFVMHjmXNYW+wWtsMrlMvyyOW4h3FB9irpn70lx7k9AeDttW4w70DgWd3+\r
+1NmlvDi7XpL0iShcWG0dqllO5SlHsB35NG7l4PSRG823z0YbGFqkDaFK+MZx7d6XOu09Z2M8MKHb\r
+OBM1vBuAkJcuUgyHXRu3KfDp+5ycVTaeU36kKUlYOQQcEVrehvC5l1Mh/VClISHFMttIVgL45VnH\r
+TkEH4rQbjpHTbyGWVQIzL7bYabc2AnaMfYnAxk0K35Smo7e/2IRdC7eXUwfT5m6pfbtC/wARIlLW\r
+VNu7yoN9MlQ9h3NO+n9Cwo8rzZU1Sm2Mlx9YLaUkHjaOv3Nc7zd7FoyY5D07HR56SfMl7961ZGNo\r
+9gKXrtd77dnkssoSwt7K9rZG8jHU44Tkc9q0rvbyvipnNgT9kTRLvqKy2JDgS/8AiH3hjecKXjv2\r
+/SkG8akmRyhqG+hKSQ4dpyofBxxV2w+Hkuda27pMW5tcSpWxati1HJGQTkYp70xoS2MW1pp+ImXN\r
+koJLi+UtfP1FAt1dFPHcPXQ9nPUy+/3pu4usrYZS16MOKCAkuLJypRxX5aG5ExX4VlfC/Vt98e3z\r
+WvL8M9NsNMtyFyVyGx6h5uPMPyMcV9Q9HQbbdWwzHQGFHKVhStw+uTQTr6tu1IQad85M46baVarV\r
+uVkJ/mDVCVqWUll59t4FxlW0ocOA4k+1P8uLGU35UgAhQ2kgdRWUeIMi2WyKqASFLJJbWchQI7Ul\r
+pWWyw5GSYZ1IXA4Ez7U12mR7q95jCWgTuCQeoPsaGqntylbCpIdxnaSM/wBK56lujtydZS4UkNIw\r
+CBzQO4RURywWnUupcQF7knoT1BHYg5r0lFY2DIwZKvYq5x1DjUo26WzJKEuIQoFSFDIP+9bzaL0x\r
++HZcZcQpC0ggewIrzYzNJQGpGVt+/cUw2PU8+0vqWEJnW8q/9KzgpHslXb6UV6yw4gBZg8z1NZbj\r
+Ek43LQDjkZFMLbkMcJW3+orKvDq86T1SUssrEef3iPq2rz8f3vtTZrtizaR0pOvD8XephOG2959a\r
+ycJH60HBBxDBhjMB+L9/RY7WpT7jam3kkNNJwSs+/NSss0Bpi4+Jmpfxl7kPOQ2k7iCfyI/hQOwz\r
+/vUroqrUnceZ8LnIG2Cdaa61Dq54i7SVJi5ymGwdjSf/ANe/86s6W0TLvkNySp5pcVjBUy0oAD5x\r
+1P1NbDbPALTQjp/aC5bj+OS27tH+VOmjPDqw6QEv9lNPFcpIQ4p5zeSB0A/WtNYoXCwK1nOWgjwk\r
+sFrg2wuJjtKl5IJUBwPakLxDXbNI6/alaGW6b87uL1vjJCmAogjcvHTrnb8DpVnxj1q1oOS7b9PP\r
+j9qSEErA58gHuf8AF7CsStOurpBjKZioQqS6sqU+vlayepPvQytu3cgz/fEPWaXfFjYEfLlo5+bM\r
+/aurr+X33vW6lIJUD/dyen2p80zboMNG6NBEGOygJLy04cdAGRjjn5NYRD1NcjMMme8XpST6Q4Mp\r
+H0HStstF4kO2lMS5vAlTfq9O04PQZ+KifILaqg3PnPodS5o0S3I0q4x2T3Kr+obzH1HsjuFFpeUU\r
+B5s5Snck4ST0z0p502w5HZW86qW5lXLbpSeMfHFZH4gpFutbDlrmNtujlxvzc705HAHfB5qknVSI\r
+VliuWK7STcHVBL7Ticc8c8f70IaMaipWq4z+oo6jT2sr8ma3qCfBky48be4zvcAOB6gR/CMd6EXF\r
+m9EPKhx3Vx92EJdADmOmQKJ2y5xVpiJlW+OzPSj1LbSBtURyoGjFzWqPbHljClFBLbiBnHHUmpeT\r
+WdqiPISuDM/e0bark4YzkEJkJ9RebGF7u+T/AKVeg6DbVdXHJ6U/hi35KAlRGU44zj/WrtpdfSlt\r
+D7m54jKznr/WnOAVKa9Y7cGtDVWodhaH1WnVlD7cZxPhq3NMobbeBeZQnalKlZ47cUQDSGtvlqwn\r
+GEp7AVQdbddWQHkp2dOea6qWHQlPmJSscEE9aET/AJCK/X+JFxUtuKecHnKxx8VXRKiBSkuKII55\r
+PSvq4yUQmf3qspxwc8is71fqZMeKtTO0AHn3V8UaitrDgdmcdtoyZ215q1USShq0bZClghTYPqFL\r
+Vr0xH1otbt1XKZkpT6cccfOaF6SZkz7q7dZYWHjz0ykJp2Yvi4YaYVHdUXjs2eSUlR7HPt89KoW5\r
+p8af5D3OVLldz9GLmsNLR1WZiI+oJlRB5aHgBuKe2cdaxd5tVsuy0OJbdWwvkKGUq+or0PqiyXVy\r
+IJ7za1NlIJbz6m/fgdv61lN000qWJ09EWQ8++6lqM01k8geokY5p/wCK1RXK2Nn/AOz75PS1vStt\r
+Y594iCUnOauWi5SLXMDzIQ4g8ONOp3IcT7KHcVduWn7nbWg5OgSI6SopBcQUjPtzXK1RX1OqkMtb\r
+0xcPO9PSkHrzV0WKRkHM86a2BwZqFm0da9c2pdw0asM3JgBT9qdd2uNH+8y51x7A/rSjrXUmq129\r
+Om9TuyvKhu70NyUYd4GBlX8QofG1hcLbrBF/tZ/DvtqGEDhJQONpA6gjrXq61f8AS/jDo9mXNhNu\r
+nGxxPR2O5jkBXX+tY3bcFhPtoPAin4H6gsMTQgLEhtM7eoyGioBYI4Tx7Yx+pqUr668ILjZXDOtS\r
+XZsdvlMiGkJlND/GgYDg+Rg1KwUDHIM2r7Bgiei5NwiQo635cllllAypbiwAPvWO678c4UJuRH0y\r
+gSHkDBkrHpz2CR3+prHbXJ1L4o6matwkKaYP7xzkhthsdVEf8NLWrzbo94fh2RKjAjqLSHFnKniO\r
+Cs/X/KuLSAcN3OfYW5HUD3SXJutxfnTnVOyn1lbi1HJJNPnh9otyfbJF5lLabjpJQ0FjlZHUis9C\r
+lDOO9bdHkS4WkbXBlIMdaGUnyhwkjqFfU5pf5K566gqe+I98TpBqb9pnB/Q9wu7kdyOGUNNp3oWp\r
+Owq7+3P1r9uQmqllqS+S+ghClFWR+vtT/Z7goWGOopbjodwEltQOcdR16/WrcrTFmW4tyYZHmuDc\r
+dhwkDHSvNvq2BC2+up6PThdIzDvMypelJN2lI8+M9JKxsZS1/Cfcn2+tF9K6Oh6ZeW5fYS5VwKgl\r
+locpR3Cvk0+zJTdtioi2htDe5OVL/KAPcn3r5j3ZtdmkrKFTFJ3EDG7BAzgH9a+XX2sNi8CJXaZW\r
+c3GIN7u0u931+KwhaGGspKQMKcKepVV5UmU1DZZtzspMVKQXm3F5B+gHIH0zQCBImKuiJMeCuEH1\r
+YCfVkjv+bqSKr6t1U7a7uxEgurS0yMLBASc/arlenBULiSGtOSSY6WKJKXckJU2tplSt6FA7gfvW\r
+gxA/sUBggDGSayGya5ed8tkNqSlXVYOVVpEZydIablRFF6ORgjGFJPyKga3Tuj5Il2rVC6sKT1L9\r
+tiuPTnDI3eSfc/lqrqWOuHFK4qlF1HIX7j2NWIkyQ8XEApSUcD/Ea5TmZj2SggqUMKSrp9KUByQM\r
+T45U5mSS9UzJMtMZ93GFcqJ7UL8Q3UOOww24Bx6h3V8/Sqev0sx7u4IqkB5w8tJ4KFfNBXG3Fuo/\r
+FPqLxA3FXXHtXp9PQiBXXiTGZrmIjTo68qh+Y2ygPhYSAlXIBz1rYHp04RkNRnWDOA5KyEgDrgVh\r
+mmSmPcCfQpWCACnINFdRXOW3GQ4+60GgcJKDgr+R70lqdP8AZaAvuUK3woDY4mqyrjeFWppZZUXW\r
+lnzUlYCVp+K+LLeYEoLLG5lGdxQk4wcfyrOourlyIzbDhcKVNhHB7e9XYlxatbam0dVDOAOT96Rf\r
+TEDBHMMpU9dTQpVxiTWXGUqDy1n0hxCSAPvXnfWVtnWO9TI8lpLHnZOGxhKkE54+K1K1XhLj4S4j\r
+GOnxX5qiNZ7wlpd1Di30ZS0hKtu4kdCaN8fqG0luxhwYtrdOtqZXsTA1dTWh+B+unNG6tbTIWTap\r
+hDUhGeE56L+oP8qSbtBXDnyWSB+7WUnadwH3rgYT6IQmEpS0VbU5WNyj8DrXr/F1/ueXIZT1P6Hh\r
+aVoSpJBSoZBB4IqVjPgP4ii72eHZLsSJrCPKadP8YA4B+cfrUpMgg4jK8jMybw5vUfT/AIXatujD\r
+iRc5S24DX95KVAkn/P8ASstODk9asPSXvwZbUEoQpzhtIwkYHt9z1q3NZiO2uNMhFLbif3chkryc\r
+9lAHsabbAbP5i6DI/qctPSokW9w3p0cvsIcBLY7+2fituuVxYvDbAMZ2VIUkeX5I5x3Tgdqznwz0\r
+xbb/ADZQuy3w2y2FISycHJz3+MVtWnNLwNMb3G0SZDvlgb3DlWPgf86V5/5e+oOAc7l/9y18WLK/\r
+IdH/AHB+l23bLPLMl0RkyQS22r1eWQO/tR178NEju3GS8ZahyVIc7ewA4qpKKfxzTMOGHCsBZSob\r
+ueveitut+XGo8tpDacEp2DAP69ahNYHO4yo1rMxJgt22RLy0l5bYQ04jckLWfM+o7frVPUMpdg0a\r
+65EfXvaX5XOArnp9hTtGgRbcyhL6PPbaG1ClnJAPvWeeMl0FogwnWGYkqKHSFxnUkpSojgkD79aJ\r
+pQbblr9ZgNRcAhMzli9zZYfS27NkPBIKAFKVnnkn2pf1PaZbMNm4PpkDzeV+c0UEK+p6/WtX8H5M\r
+GXDm3OS22Jq3P/W2AlIHwOgFVPF+VBfjqKi4sEHBKSAVfFegXWsmo+pV4zJZ0wareTFbw71Y1Ab/\r
+AAjbcNh1Q/8Ae9yaYU33VESW5KdK1wucuMpwgj3FYq4S456E7VDjimGHqa6wYqIS5HmMq42LOQBT\r
+Wo0AYll5z+YCjV7MA+puVmuDkgh7evZt3bsdK46s1uiNZSY6iHwSj82CPnFC7PcbdbdOxkPTiqaB\r
+5iQlXCf61mV9uC79dn39oDIVztGAajafRK9pPoSrZezKAOzKclyXcLgue8VLUo7sHrUaVIfeCloG\r
+T0Uo9qstKdbcBLZUg9DiuzkbY4VDIBGQkdBVkuBxOrRtAwf7naKlyMoqQ4pRI9RHH2qtc1/i/KS+\r
+p3yWchtKwcIzX7HnoQv1nbgYUR7+9NESXCmR1xdjexxOXCTg9ODSzO1bBiJvCsCBFu3eahwltCnA\r
+O6ATj6082K2rlltyXGSsIGEhzPP1xQa1QJNngLmMuNPMrPKE5BwKuzrw6Yu6JJVGWkZSkHIXn274\r
+pe8m0+H+51G2DBlu4J/DzFKbWhICiS2EgH7H2FD3JTMuclt7B2ArBzgJPvQNF1lSUFoON5JyST1P\r
+tmgEu5yY0wgJ2uoUd27nPtRKdEzHk8xezVLUnHudtXsRYc4rt8pxZdKvMSpWcH60M07a03W5JZcW\r
+UtgFSj8Dt96orKnVKUQVK6nv966R5b0dCksLLe4gkp68dOatKjBNgPMiM4Z9xHE1fwCkQx4pqYdC\r
+vJcC1RwT0WkZH8s1KVPDm+Psa208ogAtysqWOqyo4JP2qUtanPM2jDEL+OWn49u8R5UK0MbGClDg\r
+bSOApYyQPvSzM0rKt9qiXCRs8uSSlCeQoHnII+1aJ/aAZWjxImL3FILTSwR/+RX7bhqJ561XC5Jj\r
+O20pSnyFYJWMZypJ6djWLdSa1BzxDUaYWnaOzH/RlmZ0nYWPJab9SQqS5t/eLV2+wzj7UfZmouM8\r
+MNtlsNoKlFZAV8H4FULPfmrmtyCtwJfQjKggFIVx2orHsbUZ1TzCktFwfvVKJJUB05968jqHaxyz\r
+y3t+sBeiJJTLSXA6hAWscFSTjke561yfkAlte4h88BIJwB3q5Hjx297RUpWfUD+YYqs5Gjx3HJJK\r
+ywRylIGM+/vShBMIrDMtpKiyVKcWtvaP3aRnn3HevOfi9eZM/UEiEv8A7eOHgkhfT0jg4+5r0JJu\r
+ENLad0plpWM9c8dqUtTaMtGoJS37gyXH3UANyEHH6iqXx99entD2CK31m1CqmZZomd+HjORbXte8\r
+hOVLSk4USeTRm4xrvqbTjseUGmozTmVPLH5fgfNNNhYtWmJardbw3tf59XqIwepNM2poyJVpdKEt\r
++SRuCR/EfemLdWou3oO/cJXVmsI08z3BiFp7UakMuonR0jk47+31oG7iTM/dkNoWvCdx/KCe9P8A\r
+dIzR1PAZfjtI3gx3QsAJHznFKOqbfbbXKSzbriZrwJ8390UJRjpgnrXpdNeLAM9kSDqKDWT+AYcu\r
+1ivcK2x1KdiyYSejrCgSnPZXehTLqou7cghKRkgd6Px9SWp2xsMT23HF7QgpaOCFDoaCxFee4UKC\r
+gCT14P3oKs5B+xccx+kIpG0wlaJKZLB9KglB5Uo9KsLeDj2GzjI+1AjmPLH4ZzCVEApPAIopGCFR\r
+1rSpW4naaFbWB5DqUabMnaYEuTGyc40le4deO1fMZam17krwAOua7yYjyZCiG8hZ65ya57WW3W2y\r
+lS3FDkFW0CmgdygdydZ4MT1HezzUy4iCwVKLKcFtSuD74r9uVtRJabLZ8obckpTlP60ItSLXOeDT\r
+KlR1spG9W7clw/ejN4mXa0MDYA9FLn7olIxtxyFCprVkWbU7/cY+0FNx6/UU70GYDBQw6FrUcAgH\r
+ke9Lq3FHkkk980xXedHuYWt6D5L4A2rQrCQO4xV+yaaiTrW5JL29GRgflUCOoJ5wPmqaOKUy/cl3\r
+Zufw6itbriuAJHloSVPNlvJ/hB61RCwVAKPHc1YubQZmvNpSlKUqIACtwH371Tzk/FOKAeR7ibEj\r
+g+o06QWy7riziG2pDf4lsJCjknnrUrv4TtIe1/ZQ50Q+Fk/TkfzxUpW7ggQ1a7xmbF/aGsKEX83N\r
+U4IU8wFJZWMbtvBwf04pOieITadOMxXmWRJR6CsD1HHTH2xWx/2irAu9aJTIjJJkQXgsYHJSrg/6\r
+V5os1rjsynVXOQY8uMsER1t8r+M9j0pSymu1P/J6j+ktatxtE23QtvmwYar3cX0JjyE+hhQ9ROeC\r
+a0CJJaLTe+Uhfm/l7/YUhWKUxfbKxCztdQkJStWdySf7o/rTHZLC7bW3g5M819Y2pLiPy/TmvLak\r
+AsSeCPUp7i1hB6h+Ytbnl+US2AfVx/nXyWg4kpeOQ4CPT2FVX0JacS6qWpASnC0qIINDLlKKGyGp\r
+QaLmADgYA74xzSY7zDpWW4Eq2e0N2yXMdmKS6twlCUO4IQj3+po86RGWzGjtNgO4AATwlPXNAmPK\r
+dLanH15K04SEE5x7GrsGWLnclJ9SHGuCrOCU+1E2s5zNfSE/7mJniFFciyHJ6XEktoIylWBjPPHv\r
+SnC1HKlFK25Kls7cBpSvy4PtWwXHSsCXIUqUt15Tg2qStfpx7kUIc0JZIqHlpGwqTgFJxgZzx809\r
+XfWE22DJgwQD49TGr0pN2nlL7i2JKjvC1DCc9qUtRR47sjLQWiYkYdbX0PyDWwax09bZpcZtpdbl\r
+FJO5aztJxkD46Vl83TclMT8SlDjh28lIJwfY/NXdDqK8Ag4iGsosYHK8QVKiRIztv/BqccWUhT6l\r
+jASruBVpEoKkOAYLhJO0D9KGIUoqQ2vucYPaidptb0i6lCMNt8lSlq/N8VRcDblz1J9Tbf4CEGYb\r
+rzbjiEBLqQQAtQAzUs7jrqnGFNJy0fUMcA/WjlutUySrLT0dLGw5C08hQ6fbNCrTBuVlubjjkJ58\r
+pJwU5Lef72B1pQMLFYZGY0bHQggS7KYUw35ivUlXU9xSfdCp5QWltSUp/iPfNaBLtv4KGiVOkYcf\r
+X5imS2dyE9uM8DvjrQc2hyYsg+WGSfSQKxRatfJMLepvXA7iilxtKmlMJcQ4nlSlKzn7U4wbou7Y\r
+RK9SGeUpzjJPciuLmi5ayDF8t3nsrHFfFx0lcbeSptYWhKUlS0EjBP8ADR2votx5DMSFF1eRjiGF\r
+OWuK4mO+y2lTyFIWpw5SCeivgZpNuCzBU4zEmBbTnUtq4UP+ZoxaNIXG6So5ebX5C3NillXQd/pV\r
+zWlmYtEJmEiARLz6XEerf78jrXy3VK4XO4mDsSzbwMYiQI8iQlx5tpa2kfmWBwK4BKVdDiicpq5t\r
+NGItl1DbbYdUgDgAjO40JZSpxwBA5zVBDnn1EnGD+5rn9n+1pXeZlzcQFIYbCEEjoo9x9galN/hp\r
+BFn06wwQA89+9cPfJ7fpUpG072zHql2Libtf225NukRX+WnWyhX0Iry9drM3ar2i4XN0h6BKS28r\r
+O5TiByleD8Yr0ldJyHWtyOD0UKzHW9taloXM8jzkhBbkN4yVt+4HunqPvQXBxkTqH1E2dck2u5wp\r
+9rUW0yiVPKCdwQgkYJx361pca9NSGG3C5kIR6nkD0g/Ws5uMMT4DJtFyZTCdSlAjlsJKTnHpP+hr\r
+hapk+yxP2fNW7+DeSrAIyN3uP0qJfQtij8/9lPTlkznmPNwdh3FgILzgcK/3bqSfUfZQpW1BMuNr\r
+hKeeQlCyrCWeu0DjdXL9oW2NAadjuLbdj4UFBQIWoe6Scg/NEo5cu81h+5JAQtvcgdE++Tmlvr+o\r
+5YZEbpvstyvRlPSGtFvNJjzox4JKHknHP0pq03c2GlTAp5j8Spw7d5CVEYHANL9xsrTbMibHUCUJ\r
+IKEt8JPvxSey4ZylLX/8yOSMbqIK67stXwIT0NxyZubSDKUX1lbawkAZ9u+KHXeez5ja3HwhpPxy\r
+D2HNZu1rG7W5zeqS0EgbUggHA+nvVaNqOXdr5HVNcQhCV71BKQNx7ZzxQxoW7PUIgGcmNs6SqW+W\r
+2hvdc53qRgkHgc0YsdpVGgluSGygrUdqQClJ+TXVu2sSSu4x3PxD20qDa14yccAe2KruPvNw23Lg\r
+z+HDytqh1Chjoo9utAJ9LC22h0CqMRc15omyXhCnLc0mLc0c7mcBKiBnCk/PuKy646YvkCU0qLuL\r
+iWylQUPyE9cH5/WtkRLs0VhTLzqW22sEqLm5xXPTjtV2bLt88sttrCSpQxsOSCPeqGn191ACnyH7\r
+k27RI/K8TFdFOOYcTcAWENqIcUpJBz23DvTqvWMRElm3uQiUpIQ08BgJV259qdFWjzorsd8RXQ7k\r
+KJHCh7E9yBWWatszVpmsKRuCRgJTn0g5P9KKt9WrtJYYM+q07IgQGWpsNN/lsTH5W7yF7H22+Nqc\r
+ZJz84r8sMda284IRztBHal19yRbslgltMjKVA01abvCmLamK6AprbtGeoo1ysKwF5Eao0TsxK9xu\r
+03BS6hS9gU4DzkUWj26G4osKbSpRysBQJGaE2W822NHDbyngM7s4wM/avmZqdhrelhorSoEbxknn\r
+5qVtctnEOdLZnkQvKjIhuNojNZyraQMYTx1PtXzeYMZtDS30IS4lQWhWMkH4+tIxvz8GT5iQt1Bz\r
+vSoHBPbNVjPvGo33HWnSEsgqTgcE9NtMJpWyGJwJ9dQVGOxAGt9QruazbYxQGMAOOjBUo9hn4pf0\r
+vYiu7AvEKQ0rcQOh9hX47bJMW5qjlrCyohKSoEgfOKboflWmIhhsb5S+Sfk16SsCmsLX1PLWoXsz\r
+Z2I6QZ3kBKc5dPGPapSw28qMn1q3PK/Mc9PipQ4YVMwyJt2oHV2uZuGVML/mKoKWlwbkHchQ4qkN\r
+ZaevsQxzcmQsj0byUkH71TgOvRVqbeG6Ks+l5PqSD9RXxBioihqTS8Vm7JlNyHGIqlZWWujDmQQr\r
+H9339q/bihUVLqVvh1ak7S6g8KHwO1OshQIIUAoHg96z7VdpkxIEw2chTDqTmOr/AOZ90Ht9KWv0\r
+7WkYMf0Oqr075sXIgLTkZl7Uy1zZCQhpsuDOOuQOa05NvYkS0J8h1UUDd5w5UOOAfisK026yJZj3\r
+YOR3i56XRzkn+EitUsN4uEvEeCpDCGlEOL67ldMikfk6HUg54Ef02pS9i6jEcLpcGUMLSW9iU43J\r
+6EjH+VZ9NuLDmQqCIsdxR7e30rQWNPKaebmOTVrdXysq5C+OhFfcm129Y/7ptghJ3JKU8j6VLqtS\r
+rvmNFNx4mNXGMy6jEQqeUF5V8D2oS63JalpaQdrhxjdyQK2O6Ls8SOGm0hO7ohKeVH2FIl205Pdd\r
+cmMskrICkNg+pIz0IqrptWGGDwP3M3VhFye4w2hmVGYaUmUUsrwcpOSn5xTpcpUJu1vOmQpwObUK\r
+S6njfnjjtzWOu6iu3luRnIhQGTtJHBB/pRq1u3G5hhKFlIVneVdz9+lKXaRgdzkCdRxYMg9S9qB+\r
+A/MS0tpYIVudaZTgOqwAPtUdjTkORXGmhHbKgltKVBJSMd+9Mtv/ABrcWRFLUdxATl0lGFlWOx7/\r
+AAaEOJhuLZipYdksr6BokraVnnd7VhbOl7xBfWwctnj8T9m39strVFa9aMggZKlK+lLGpXLhc47d\r
+smsKjlSgpJWg5A65B7dfrWk2vTdus8p+clS1vYyEurB2H+pqs9erVc32zJIbeZXtS2oZO8fH+tap\r
+sVH3VrnHucXftIeZf/0zdZDYbKlPlpJWVnkZ7D704WLRhTbkOzg6XVpxsB2+Wfr3p0hzIylPPtth\r
+KEr2uFQxuI7ChV61IhaTGay24okBST0J6GutrLLPACMJY6DxMze/Ldtdzcik7gnlJ+DVJF2KTlVO\r
+0O2M3WK8mQ0h5/HoIOFdepPalq5aTuapziQhptrPUkHA609VZW3i3cbHyRVfKU03RLishXIpfVqe\r
+Q2lyJC/dZWQpfzmqF5f/AGdcSw08hwJxnb3V7CqcNl5qWp6U2lKRnYnOefeqlOjQDcw4kX5D5g2Y\r
+Wn13GOKsQklxR8yU51UecUSt+5GX3vU8rue1CbeypxfnO/YUWB9jRGIHAiVNZc72lgLJVzzUrmg1\r
+KFiOjjqIwUpPKSR96KWnUl1tLoXCmOt+4CuD9qFlOe9fm3nrT5wexPN5I6msWHxHjzili+Nhlw4A\r
+faGBn5HSmicCI6X2loeiufkeb5Sf6GvPqknrTJpPVs2wPbMh+EvhxhzlKh9KA1XtYZbM9xj1Laos\r
+/K1ICHv74/1qnbryuwBtCIYQgDatbayQv5wehpnu8NiXaBebK6X7csgOIPK4yj/Cr49jSbJXwQel\r
+BesWLseGrsNTbkjx/wBWQ4FvYfdntLW8NwZC8qT9RQ9Gq3bo8ERlBDajgrJ/KPekB1ltLqZCAlK0\r
+HcCUgjP0NfIuy1Tg+yw2y4kEL8kYSv52nj9KSPxNQ/jyZRr+UYfyGJt+nm7Kje95pflEAFxR6H/C\r
+DQW+OSocpBjL/EFZOHmzyR7GkzSl9ZLr5uE2LFBOPLWlWSPccYFaxpS8WZlP4aEpDri8OKO4KBP+\r
+lTL9NZQ/kMxg21agBi3MXo9ulOvB1uC8p0j1LV0PH86JQ7QpiSh94mO3tUFBSeMn2zTsJjKFrde8\r
+g8DbsIJA78VzbuEd6MVLaSWFZSCUZI985pRnJjCviI2nbncJNzXDUhL7aSU5C8J2/OKcbTaodsU7\r
+K8hLL6zuUndkA/GaU7tM/ZUlQjBlu3bdzbkdHKTnkE+59qU77q+4zISmGY8lbyVH96hKjlPHHFGG\r
+me0+HAM7bcmMxv1V/wCQkLFvcdxzktd6RbNDC71lDgbS2dy3F9sHmh8PVF5ZQtEdteFDar0eof0o\r
+8q7abXHYNxdDEhgYUUnYpffkdxmqFelspGMZz+Io2qQ+51v9/wDw7KkwZflxlElIKgTnPJNcH7mz\r
+Asjbi1smU8QouE/PBH2pd1DreyOwnojMGPIK8+tLe3HGAfrSE9cVrjtJjFfozwv1bfpnj+VOaf40\r
+so3DETv+RReF5m53LUNis0Bp9ExK3QkAoQ5nPfisq1druXd3CmMVtsDITlXOPn3pcMGS/HW84VKd\r
+zwF9SKFKCs7T27U/pvjqaju7Mm6jW2uMdCE4tsukyI5cmY77sdtYSt4DICuoBNMFoWiapJcVhY6o\r
+V7138N9XK0/JWw42l+BIT5cmMv8AK6jv9COxpi1XpBtE2LctJvfi7bOBdbAI8xrH5krHYj370zaf\r
+R4gqCQwxzOCMJGE9K6A4rm20ttnDysuJ4OBxmq0uWllv08rNIjyOBPRsCg5GJLnODDZQg+s/yqUs\r
+zJKlqUVHJNSmkqGOZOt1TBvGfZIxkVwWsg1KlaEmT8DhxX7u3dqlStTka/D3Ur2nrylKkfiIEr9z\r
+IjK/K4g9fvR/xBsyLDqF+IwsrjqSl5rd1CFjcAfkZqVKHYIZOonyclpZz0oeygoUpWetSpWVmz1O\r
+c6Ol9o9lDoaBIkPMOZS4obTg4URUqUzWAeDE7SVPEYrXrSZb30ORGwhwDG4rUr/M0SXri+SpYcYu\r
+EiMMcJbVx9alSgtpad27aMw6ai0pjdKFz1nqJuSn/wAtIJIznj+lfQu11VueVdJm9weohwjNSpWj\r
+UigYAmfsck8wPPlPKz5jzyz33LJoOt1SieSB7VKlGQQDk5n2w35qwCaYLbEQEBwgY7CpUrlphaAC\r
+3MIkBKc0DuUUKC5CcJIPI96lSh18GH1AyINiI8x9CM4x3Fat4f6okWOY0qKkFv8AKpCgCFp75qVK\r
+xqfUY+MUENmMmv7bHbDV5tqPJjTFcsK6pVgE4+Kz68xy41vZUEKPvUqUovDyufKjmfrVmYbiHd6n\r
+cbis+/WpUqUcMZKdF44n/9k=\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270--\r
+\r
--- /dev/null
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project: onlinestore
+Tracker: Feature request
+category: Stock management
+priority: Urgent
--- /dev/null
+Return-Path: <JSmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <JSmith@somenet.foo>
+To: <redmine@somenet.foo>
+Cc: <DLopper@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
--- /dev/null
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket with custom field values
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+category: Stock management
+searchable field: Value for a custom field
--- /dev/null
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project : onlinestore
+Tracker: Feature request
+category : Stock management
+priority: Urgent
--- /dev/null
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project: onlinestore
+Status: Resolved
+
--- /dev/null
+---
+member_roles_001:
+ id: 1
+ role_id: 1
+ member_id: 1
+member_roles_002:
+ id: 2
+ role_id: 2
+ member_id: 2
+member_roles_003:
+ id: 3
+ role_id: 2
+ member_id: 3
+member_roles_004:
+ id: 4
+ role_id: 2
+ member_id: 4
+member_roles_005:
+ id: 5
+ role_id: 1
+ member_id: 5
+member_roles_006:
+ id: 6
+ role_id: 1
+ member_id: 6
+member_roles_007:
+ id: 7
+ role_id: 2
+ member_id: 6
+member_roles_008:
+ id: 8
+ role_id: 1
+ member_id: 7
+ inherited_from: 6
+member_roles_009:
+ id: 9
+ role_id: 2
+ member_id: 7
+ inherited_from: 7
--- /dev/null
+---
+members_001:
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 1
+ id: 1
+ user_id: 2
+ mail_notification: true
+members_002:
+ created_on: 2006-07-19 19:35:36 +02:00
+ project_id: 1
+ id: 2
+ user_id: 3
+ mail_notification: true
+members_003:
+ created_on: 2006-07-19 19:35:36 +02:00
+ project_id: 2
+ id: 3
+ user_id: 2
+ mail_notification: true
+members_004:
+ id: 4
+ created_on: 2006-07-19 19:35:36 +02:00
+ project_id: 1
+ # Locked user
+ user_id: 5
+ mail_notification: true
+members_005:
+ id: 5
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ user_id: 2
+ mail_notification: true
+members_006:
+ id: 6
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ user_id: 10
+ mail_notification: false
+members_007:
+ id: 7
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ user_id: 8
+ mail_notification: false
+
\ No newline at end of file
--- /dev/null
+---
+messages_001:
+ created_on: 2007-05-12 17:15:32 +02:00
+ updated_on: 2007-05-12 17:15:32 +02:00
+ subject: First post
+ id: 1
+ replies_count: 2
+ last_reply_id: 3
+ content: "This is the very first post\n\
+ in the forum"
+ author_id: 1
+ parent_id:
+ board_id: 1
+messages_002:
+ created_on: 2007-05-12 17:18:00 +02:00
+ updated_on: 2007-05-12 17:18:00 +02:00
+ subject: First reply
+ id: 2
+ replies_count: 0
+ last_reply_id:
+ content: "Reply to the first post"
+ author_id: 1
+ parent_id: 1
+ board_id: 1
+messages_003:
+ created_on: 2007-05-12 17:18:02 +02:00
+ updated_on: 2007-05-12 17:18:02 +02:00
+ subject: "RE: First post"
+ id: 3
+ replies_count: 0
+ last_reply_id:
+ content: "An other reply"
+ author_id: 2
+ parent_id: 1
+ board_id: 1
+messages_004:
+ created_on: 2007-08-12 17:15:32 +02:00
+ updated_on: 2007-08-12 17:15:32 +02:00
+ subject: Post 2
+ id: 4
+ replies_count: 2
+ last_reply_id: 6
+ content: "This is an other post"
+ author_id:
+ parent_id:
+ board_id: 1
+messages_005:
+ created_on: <%= 3.days.ago.to_date.to_s(:db) %>
+ updated_on: <%= 3.days.ago.to_date.to_s(:db) %>
+ subject: 'RE: post 2'
+ id: 5
+ replies_count: 0
+ last_reply_id:
+ content: "Reply to the second post"
+ author_id: 1
+ parent_id: 4
+ board_id: 1
+messages_006:
+ created_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ subject: 'RE: post 2'
+ id: 6
+ replies_count: 0
+ last_reply_id:
+ content: "Another reply to the second post"
+ author_id: 3
+ parent_id: 4
+ board_id: 1
--- /dev/null
+---
+news_001:
+ created_on: 2006-07-19 22:40:26 +02:00
+ project_id: 1
+ title: eCookbook first release !
+ id: 1
+ description: |-
+ eCookbook 1.0 has been released.
+
+ Visit http://ecookbook.somenet.foo/
+ summary: First version was released...
+ author_id: 2
+ comments_count: 1
+news_002:
+ created_on: 2006-07-19 22:42:58 +02:00
+ project_id: 1
+ title: 100,000 downloads for eCookbook
+ id: 2
+ description: eCookbook 1.0 have downloaded 100,000 times
+ summary: eCookbook 1.0 have downloaded 100,000 times
+ author_id: 2
+ comments_count: 0
--- /dev/null
+---
+projects_001:
+ created_on: 2006-07-19 19:13:59 +02:00
+ name: eCookbook
+ updated_on: 2006-07-19 22:53:01 +02:00
+ id: 1
+ description: Recipes management application
+ homepage: http://ecookbook.somenet.foo/
+ is_public: true
+ identifier: ecookbook
+ parent_id:
+ lft: 1
+ rgt: 10
+projects_002:
+ created_on: 2006-07-19 19:14:19 +02:00
+ name: OnlineStore
+ updated_on: 2006-07-19 19:14:19 +02:00
+ id: 2
+ description: E-commerce web site
+ homepage: ""
+ is_public: false
+ identifier: onlinestore
+ parent_id:
+ lft: 11
+ rgt: 12
+projects_003:
+ created_on: 2006-07-19 19:15:21 +02:00
+ name: eCookbook Subproject 1
+ updated_on: 2006-07-19 19:18:12 +02:00
+ id: 3
+ description: eCookBook Subproject 1
+ homepage: ""
+ is_public: true
+ identifier: subproject1
+ parent_id: 1
+ lft: 6
+ rgt: 7
+projects_004:
+ created_on: 2006-07-19 19:15:51 +02:00
+ name: eCookbook Subproject 2
+ updated_on: 2006-07-19 19:17:07 +02:00
+ id: 4
+ description: eCookbook Subproject 2
+ homepage: ""
+ is_public: true
+ identifier: subproject2
+ parent_id: 1
+ lft: 8
+ rgt: 9
+projects_005:
+ created_on: 2006-07-19 19:15:51 +02:00
+ name: Private child of eCookbook
+ updated_on: 2006-07-19 19:17:07 +02:00
+ id: 5
+ description: This is a private subproject of a public project
+ homepage: ""
+ is_public: false
+ identifier: private-child
+ parent_id: 1
+ lft: 2
+ rgt: 5
+projects_006:
+ created_on: 2006-07-19 19:15:51 +02:00
+ name: Child of private child
+ updated_on: 2006-07-19 19:17:07 +02:00
+ id: 6
+ description: This is a public subproject of a private project
+ homepage: ""
+ is_public: true
+ identifier: project6
+ parent_id: 5
+ lft: 3
+ rgt: 4
+
\ No newline at end of file
--- /dev/null
+---
+projects_trackers_001:
+ project_id: 4
+ tracker_id: 3
+projects_trackers_002:
+ project_id: 1
+ tracker_id: 1
+projects_trackers_003:
+ project_id: 5
+ tracker_id: 1
+projects_trackers_004:
+ project_id: 1
+ tracker_id: 2
+projects_trackers_005:
+ project_id: 5
+ tracker_id: 2
+projects_trackers_006:
+ project_id: 5
+ tracker_id: 3
+projects_trackers_007:
+ project_id: 2
+ tracker_id: 1
+projects_trackers_008:
+ project_id: 2
+ tracker_id: 2
+projects_trackers_009:
+ project_id: 2
+ tracker_id: 3
+projects_trackers_010:
+ project_id: 3
+ tracker_id: 2
+projects_trackers_011:
+ project_id: 3
+ tracker_id: 3
+projects_trackers_012:
+ project_id: 4
+ tracker_id: 1
+projects_trackers_013:
+ project_id: 4
+ tracker_id: 2
+projects_trackers_014:
+ project_id: 1
+ tracker_id: 3
+
\ No newline at end of file
--- /dev/null
+---
+queries_001:
+ id: 1
+ project_id: 1
+ is_public: true
+ 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
+ project_id: 1
+ is_public: false
+ 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
+ project_id:
+ is_public: false
+ name: Private query for all projects
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 3
+ column_names:
+queries_004:
+ id: 4
+ project_id:
+ is_public: true
+ name: Public query for all projects
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 2
+ column_names:
+queries_005:
+ id: 5
+ project_id:
+ is_public: true
+ 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
+ project_id:
+ is_public: true
+ 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
+ project_id: 2
+ is_public: true
+ name: Public query for project 2
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 2
+ column_names:
+queries_008:
+ id: 8
+ project_id: 2
+ is_public: false
+ name: Private query for project 2
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 2
+ column_names:
+queries_009:
+ id: 9
+ project_id:
+ is_public: true
+ 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
+
--- /dev/null
+---
+repositories_001:
+ project_id: 1
+ url: file:///<%= RAILS_ROOT.gsub(%r{config\/\.\.}, '') %>/tmp/test/subversion_repository
+ id: 10
+ root_url: file:///<%= RAILS_ROOT.gsub(%r{config\/\.\.}, '') %>/tmp/test/subversion_repository
+ password: ""
+ login: ""
+ type: Subversion
+repositories_002:
+ project_id: 2
+ url: svn://localhost/test
+ id: 11
+ root_url: svn://localhost
+ password: ""
+ login: ""
+ type: Subversion
--- /dev/null
+---
+roles_001:
+ name: Manager
+ id: 1
+ builtin: 0
+ permissions: |
+ ---
+ - :add_project
+ - :edit_project
+ - :manage_members
+ - :manage_versions
+ - :manage_categories
+ - :view_issues
+ - :add_issues
+ - :edit_issues
+ - :manage_issue_relations
+ - :add_issue_notes
+ - :move_issues
+ - :delete_issues
+ - :view_issue_watchers
+ - :add_issue_watchers
+ - :delete_issue_watchers
+ - :manage_public_queries
+ - :save_queries
+ - :view_gantt
+ - :view_calendar
+ - :log_time
+ - :view_time_entries
+ - :edit_time_entries
+ - :delete_time_entries
+ - :manage_news
+ - :comment_news
+ - :view_documents
+ - :manage_documents
+ - :view_wiki_pages
+ - :view_wiki_edits
+ - :edit_wiki_pages
+ - :delete_wiki_pages_attachments
+ - :protect_wiki_pages
+ - :delete_wiki_pages
+ - :rename_wiki_pages
+ - :add_messages
+ - :edit_messages
+ - :delete_messages
+ - :manage_boards
+ - :view_files
+ - :manage_files
+ - :browse_repository
+ - :manage_repository
+ - :view_changesets
+ - :manage_project_activities
+
+ position: 1
+roles_002:
+ name: Developer
+ id: 2
+ builtin: 0
+ permissions: |
+ ---
+ - :edit_project
+ - :manage_members
+ - :manage_versions
+ - :manage_categories
+ - :view_issues
+ - :add_issues
+ - :edit_issues
+ - :manage_issue_relations
+ - :add_issue_notes
+ - :move_issues
+ - :delete_issues
+ - :view_issue_watchers
+ - :save_queries
+ - :view_gantt
+ - :view_calendar
+ - :log_time
+ - :view_time_entries
+ - :edit_own_time_entries
+ - :manage_news
+ - :comment_news
+ - :view_documents
+ - :manage_documents
+ - :view_wiki_pages
+ - :view_wiki_edits
+ - :edit_wiki_pages
+ - :protect_wiki_pages
+ - :delete_wiki_pages
+ - :add_messages
+ - :edit_own_messages
+ - :delete_own_messages
+ - :manage_boards
+ - :view_files
+ - :manage_files
+ - :browse_repository
+ - :view_changesets
+
+ position: 2
+roles_003:
+ name: Reporter
+ id: 3
+ builtin: 0
+ permissions: |
+ ---
+ - :edit_project
+ - :manage_members
+ - :manage_versions
+ - :manage_categories
+ - :view_issues
+ - :add_issues
+ - :edit_issues
+ - :manage_issue_relations
+ - :add_issue_notes
+ - :move_issues
+ - :view_issue_watchers
+ - :save_queries
+ - :view_gantt
+ - :view_calendar
+ - :log_time
+ - :view_time_entries
+ - :manage_news
+ - :comment_news
+ - :view_documents
+ - :manage_documents
+ - :view_wiki_pages
+ - :view_wiki_edits
+ - :edit_wiki_pages
+ - :delete_wiki_pages
+ - :add_messages
+ - :manage_boards
+ - :view_files
+ - :manage_files
+ - :browse_repository
+ - :view_changesets
+
+ position: 3
+roles_004:
+ name: Non member
+ id: 4
+ builtin: 1
+ permissions: |
+ ---
+ - :view_issues
+ - :add_issues
+ - :edit_issues
+ - :manage_issue_relations
+ - :add_issue_notes
+ - :move_issues
+ - :save_queries
+ - :view_gantt
+ - :view_calendar
+ - :log_time
+ - :view_time_entries
+ - :comment_news
+ - :view_documents
+ - :manage_documents
+ - :view_wiki_pages
+ - :view_wiki_edits
+ - :edit_wiki_pages
+ - :add_messages
+ - :view_files
+ - :manage_files
+ - :browse_repository
+ - :view_changesets
+
+ position: 4
+roles_005:
+ name: Anonymous
+ id: 5
+ builtin: 2
+ permissions: |
+ ---
+ - :view_issues
+ - :add_issue_notes
+ - :view_gantt
+ - :view_calendar
+ - :view_time_entries
+ - :view_documents
+ - :view_wiki_pages
+ - :view_wiki_edits
+ - :view_files
+ - :browse_repository
+ - :view_changesets
+
+ position: 5
+
--- /dev/null
+---
+time_entries_001:
+ created_on: 2007-03-23 12:54:18 +01:00
+ tweek: 12
+ tmonth: 3
+ project_id: 1
+ comments: My hours
+ updated_on: 2007-03-23 12:54:18 +01:00
+ activity_id: 9
+ spent_on: 2007-03-23
+ issue_id: 1
+ id: 1
+ hours: 4.25
+ user_id: 2
+ tyear: 2007
+time_entries_002:
+ created_on: 2007-03-23 14:11:04 +01:00
+ tweek: 11
+ tmonth: 3
+ project_id: 1
+ comments: ""
+ updated_on: 2007-03-23 14:11:04 +01:00
+ activity_id: 9
+ spent_on: 2007-03-12
+ issue_id: 1
+ id: 2
+ hours: 150.0
+ user_id: 1
+ tyear: 2007
+time_entries_003:
+ created_on: 2007-04-21 12:20:48 +02:00
+ tweek: 16
+ tmonth: 4
+ project_id: 1
+ comments: ""
+ updated_on: 2007-04-21 12:20:48 +02:00
+ activity_id: 9
+ spent_on: 2007-04-21
+ issue_id: 3
+ id: 3
+ hours: 1.0
+ user_id: 1
+ tyear: 2007
+time_entries_004:
+ created_on: 2007-04-22 12:20:48 +02:00
+ tweek: 16
+ tmonth: 4
+ project_id: 3
+ comments: Time spent on a subproject
+ updated_on: 2007-04-22 12:20:48 +02:00
+ activity_id: 10
+ spent_on: 2007-04-22
+ issue_id:
+ id: 4
+ hours: 7.65
+ user_id: 1
+ tyear: 2007
+
--- /dev/null
+---
+tokens_001:
+ created_on: 2007-01-21 00:39:12 +01:00
+ action: register
+ id: 1
+ value: DwMJ2yIxBNeAk26znMYzYmz5dAiIina0GFrPnGTM
+ user_id: 1
+tokens_002:
+ created_on: 2007-01-21 00:39:52 +01:00
+ action: recovery
+ id: 2
+ value: sahYSIaoYrsZUef86sTHrLISdznW6ApF36h5WSnm
+ user_id: 2
--- /dev/null
+---
+trackers_001:
+ name: Bug
+ id: 1
+ is_in_chlog: true
+ position: 1
+trackers_002:
+ name: Feature request
+ id: 2
+ is_in_chlog: true
+ position: 2
+trackers_003:
+ name: Support request
+ id: 3
+ is_in_chlog: false
+ position: 3
--- /dev/null
+---
+user_preferences_001:
+ others: |
+ ---
+ :my_page_layout:
+ left:
+ - latest_news
+ - documents
+ right:
+ - issues_assigned_to_me
+ - issues_reported_by_me
+ top:
+ - calendar
+
+ id: 1
+ user_id: 1
+ hide_mail: true
+user_preferences_002:
+ others: |+
+ --- {}
+
+ id: 2
+ user_id: 3
+ hide_mail: false
\ No newline at end of file
--- /dev/null
+---
+users_004:
+ created_on: 2006-07-19 19:34:07 +02:00
+ status: 1
+ last_login_on:
+ language: en
+ hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608
+ updated_on: 2006-07-19 19:34:07 +02:00
+ admin: false
+ mail: rhill@somenet.foo
+ lastname: Hill
+ firstname: Robert
+ id: 4
+ auth_source_id:
+ mail_notification: true
+ login: rhill
+ type: User
+users_001:
+ created_on: 2006-07-19 19:12:21 +02:00
+ status: 1
+ last_login_on: 2006-07-19 22:57:52 +02:00
+ language: en
+ hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997
+ updated_on: 2006-07-19 22:57:52 +02:00
+ admin: true
+ mail: admin@somenet.foo
+ lastname: Admin
+ firstname: redMine
+ id: 1
+ auth_source_id:
+ mail_notification: true
+ login: admin
+ type: User
+users_002:
+ created_on: 2006-07-19 19:32:09 +02:00
+ status: 1
+ last_login_on: 2006-07-19 22:42:15 +02:00
+ language: en
+ hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d
+ updated_on: 2006-07-19 22:42:15 +02:00
+ admin: false
+ mail: jsmith@somenet.foo
+ lastname: Smith
+ firstname: John
+ id: 2
+ auth_source_id:
+ mail_notification: true
+ login: jsmith
+ type: User
+users_003:
+ created_on: 2006-07-19 19:33:19 +02:00
+ status: 1
+ last_login_on:
+ language: en
+ hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: dlopper@somenet.foo
+ lastname: Lopper
+ firstname: Dave
+ id: 3
+ auth_source_id:
+ mail_notification: true
+ login: dlopper
+ type: User
+users_005:
+ id: 5
+ created_on: 2006-07-19 19:33:19 +02:00
+ # Locked
+ status: 3
+ last_login_on:
+ language: en
+ hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: dlopper2@somenet.foo
+ lastname: Lopper2
+ firstname: Dave2
+ auth_source_id:
+ mail_notification: true
+ login: dlopper2
+ type: User
+users_006:
+ id: 6
+ created_on: 2006-07-19 19:33:19 +02:00
+ status: 0
+ last_login_on:
+ language: ''
+ hashed_password: 1
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: ''
+ lastname: Anonymous
+ firstname: ''
+ auth_source_id:
+ mail_notification: false
+ login: ''
+ type: AnonymousUser
+users_007:
+ id: 7
+ created_on: 2006-07-19 19:33:19 +02:00
+ status: 1
+ last_login_on:
+ language: ''
+ hashed_password: 1
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: someone@foo.bar
+ lastname: One
+ firstname: Some
+ auth_source_id:
+ mail_notification: false
+ login: someone
+ type: User
+users_008:
+ id: 8
+ created_on: 2006-07-19 19:33:19 +02:00
+ status: 1
+ last_login_on:
+ language: 'it'
+ hashed_password: 1
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: miscuser8@foo.bar
+ lastname: Misc
+ firstname: User
+ auth_source_id:
+ mail_notification: false
+ login: miscuser8
+ type: User
+users_009:
+ id: 9
+ created_on: 2006-07-19 19:33:19 +02:00
+ status: 1
+ last_login_on:
+ language: 'it'
+ hashed_password: 1
+ updated_on: 2006-07-19 19:33:19 +02:00
+ admin: false
+ mail: miscuser9@foo.bar
+ lastname: Misc
+ firstname: User
+ auth_source_id:
+ mail_notification: false
+ login: miscuser9
+ type: User
+groups_010:
+ id: 10
+ lastname: A Team
+ type: Group
+groups_011:
+ id: 11
+ lastname: B Team
+ type: Group
+
+
\ No newline at end of file
--- /dev/null
+---
+versions_001:
+ created_on: 2006-07-19 21:00:07 +02:00
+ name: "0.1"
+ project_id: 1
+ updated_on: 2006-07-19 21:00:07 +02:00
+ id: 1
+ description: Beta
+ effective_date: 2006-07-01
+ status: closed
+versions_002:
+ created_on: 2006-07-19 21:00:33 +02:00
+ name: "1.0"
+ project_id: 1
+ updated_on: 2006-07-19 21:00:33 +02:00
+ id: 2
+ description: Stable release
+ effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
+ status: locked
+versions_003:
+ created_on: 2006-07-19 21:00:33 +02:00
+ name: "2.0"
+ project_id: 1
+ updated_on: 2006-07-19 21:00:33 +02:00
+ id: 3
+ description: Future version
+ effective_date:
+ status: open
+
\ No newline at end of file
--- /dev/null
+---
+watchers_001:
+ watchable_type: Issue
+ watchable_id: 2
+ user_id: 3
+watchers_002:
+ watchable_type: Message
+ watchable_id: 1
+ user_id: 1
+watchers_003:
+ watchable_type: Issue
+ watchable_id: 2
+ user_id: 1
+
\ No newline at end of file
--- /dev/null
+---
+wiki_content_versions_001:
+ updated_on: 2007-03-07 00:08:07 +01:00
+ page_id: 1
+ id: 1
+ version: 1
+ author_id: 2
+ comments: Page creation
+ wiki_content_id: 1
+ compression: ""
+ data: |-
+ h1. CookBook documentation
+
+
+
+ Some [[documentation]] here...
+wiki_content_versions_002:
+ updated_on: 2007-03-07 00:08:34 +01:00
+ page_id: 1
+ id: 2
+ version: 2
+ author_id: 1
+ comments: Small update
+ wiki_content_id: 1
+ compression: ""
+ data: |-
+ h1. CookBook documentation
+
+
+
+ Some updated [[documentation]] here...
+wiki_content_versions_003:
+ updated_on: 2007-03-07 00:10:51 +01:00
+ page_id: 1
+ id: 3
+ version: 3
+ author_id: 1
+ comments: ""
+ wiki_content_id: 1
+ compression: ""
+ data: |-
+ h1. CookBook documentation
+ Some updated [[documentation]] here...
+wiki_content_versions_004:
+ data: |-
+ h1. Another page
+
+ This is a link to a ticket: #2
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 2
+ wiki_content_id: 2
+ id: 4
+ version: 1
+ author_id: 1
+ comments:
+
--- /dev/null
+---
+wiki_contents_001:
+ text: |-
+ h1. CookBook documentation
+
+ {{child_pages}}
+
+ Some updated [[documentation]] here with gzipped history
+ updated_on: 2007-03-07 00:10:51 +01:00
+ page_id: 1
+ id: 1
+ version: 3
+ author_id: 1
+ comments: Gzip compression activated
+wiki_contents_002:
+ text: |-
+ h1. Another page
+
+ This is a link to a ticket: #2
+ And this is an included page:
+ {{include(Page with an inline image)}}
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 2
+ id: 2
+ version: 1
+ author_id: 1
+ comments:
+wiki_contents_003:
+ text: |-
+ h1. Start page
+
+ E-commerce web site start page
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 3
+ id: 3
+ version: 1
+ author_id: 1
+ comments:
+wiki_contents_004:
+ text: |-
+ h1. Page with an inline image
+
+ This is an inline image:
+
+ !logo.gif!
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 4
+ id: 4
+ version: 1
+ author_id: 1
+ comments:
+wiki_contents_005:
+ text: |-
+ h1. Child page 1
+
+ This is a child page
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 5
+ id: 5
+ version: 1
+ author_id: 1
+ comments:
+wiki_contents_006:
+ text: |-
+ h1. Child page 2
+
+ This is a child page
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 6
+ id: 6
+ version: 1
+ author_id: 1
+ comments:
+
\ No newline at end of file
--- /dev/null
+---
+wiki_pages_001:
+ created_on: 2007-03-07 00:08:07 +01:00
+ title: CookBook_documentation
+ id: 1
+ wiki_id: 1
+ protected: true
+ parent_id:
+wiki_pages_002:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Another_page
+ id: 2
+ wiki_id: 1
+ protected: false
+ parent_id:
+wiki_pages_003:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Start_page
+ id: 3
+ wiki_id: 2
+ protected: false
+ parent_id:
+wiki_pages_004:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Page_with_an_inline_image
+ id: 4
+ wiki_id: 1
+ protected: false
+ parent_id: 1
+wiki_pages_005:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Child_1
+ id: 5
+ wiki_id: 1
+ protected: false
+ parent_id: 2
+wiki_pages_006:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Child_2
+ id: 6
+ wiki_id: 1
+ protected: false
+ parent_id: 2
+
\ No newline at end of file
--- /dev/null
+---
+wikis_001:
+ status: 1
+ start_page: CookBook documentation
+ project_id: 1
+ id: 1
+wikis_002:
+ status: 1
+ start_page: Start page
+ project_id: 2
+ id: 2
+
\ No newline at end of file
--- /dev/null
+---
+workflows_189:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 2
+ id: 189
+ tracker_id: 3
+workflows_001:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 1
+ id: 1
+ tracker_id: 1
+workflows_002:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 1
+ id: 2
+ tracker_id: 1
+workflows_003:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 1
+ id: 3
+ tracker_id: 1
+workflows_110:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 4
+ id: 110
+ tracker_id: 2
+workflows_004:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 1
+ id: 4
+ tracker_id: 1
+workflows_030:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 6
+ id: 30
+ tracker_id: 1
+workflows_111:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 5
+ id: 111
+ tracker_id: 2
+workflows_005:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 1
+ id: 5
+ tracker_id: 1
+workflows_031:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 1
+ id: 31
+ tracker_id: 1
+workflows_112:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 5
+ id: 112
+ tracker_id: 2
+workflows_006:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 2
+ id: 6
+ tracker_id: 1
+workflows_032:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 1
+ id: 32
+ tracker_id: 1
+workflows_113:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 5
+ id: 113
+ tracker_id: 2
+workflows_220:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 2
+ id: 220
+ tracker_id: 3
+workflows_007:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 2
+ id: 7
+ tracker_id: 1
+workflows_033:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 1
+ id: 33
+ tracker_id: 1
+workflows_060:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 6
+ id: 60
+ tracker_id: 1
+workflows_114:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 5
+ id: 114
+ tracker_id: 2
+workflows_140:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 4
+ id: 140
+ tracker_id: 2
+workflows_221:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 3
+ id: 221
+ tracker_id: 3
+workflows_008:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 2
+ id: 8
+ tracker_id: 1
+workflows_034:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 1
+ id: 34
+ tracker_id: 1
+workflows_115:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 5
+ id: 115
+ tracker_id: 2
+workflows_141:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 5
+ id: 141
+ tracker_id: 2
+workflows_222:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 3
+ id: 222
+ tracker_id: 3
+workflows_223:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 3
+ id: 223
+ tracker_id: 3
+workflows_009:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 2
+ id: 9
+ tracker_id: 1
+workflows_035:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 1
+ id: 35
+ tracker_id: 1
+workflows_061:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 1
+ id: 61
+ tracker_id: 1
+workflows_116:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 6
+ id: 116
+ tracker_id: 2
+workflows_142:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 5
+ id: 142
+ tracker_id: 2
+workflows_250:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 2
+ id: 250
+ tracker_id: 3
+workflows_224:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 3
+ id: 224
+ tracker_id: 3
+workflows_036:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 2
+ id: 36
+ tracker_id: 1
+workflows_062:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 1
+ id: 62
+ tracker_id: 1
+workflows_117:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 6
+ id: 117
+ tracker_id: 2
+workflows_143:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 5
+ id: 143
+ tracker_id: 2
+workflows_170:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 4
+ id: 170
+ tracker_id: 2
+workflows_251:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 3
+ id: 251
+ tracker_id: 3
+workflows_225:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 3
+ id: 225
+ tracker_id: 3
+workflows_063:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 1
+ id: 63
+ tracker_id: 1
+workflows_090:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 6
+ id: 90
+ tracker_id: 1
+workflows_118:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 6
+ id: 118
+ tracker_id: 2
+workflows_144:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 5
+ id: 144
+ tracker_id: 2
+workflows_252:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 3
+ id: 252
+ tracker_id: 3
+workflows_226:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 4
+ id: 226
+ tracker_id: 3
+workflows_038:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 2
+ id: 38
+ tracker_id: 1
+workflows_064:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 1
+ id: 64
+ tracker_id: 1
+workflows_091:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 1
+ id: 91
+ tracker_id: 2
+workflows_119:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 6
+ id: 119
+ tracker_id: 2
+workflows_145:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 5
+ id: 145
+ tracker_id: 2
+workflows_171:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 5
+ id: 171
+ tracker_id: 2
+workflows_253:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 3
+ id: 253
+ tracker_id: 3
+workflows_227:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 4
+ id: 227
+ tracker_id: 3
+workflows_039:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 2
+ id: 39
+ tracker_id: 1
+workflows_065:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 1
+ id: 65
+ tracker_id: 1
+workflows_092:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 1
+ id: 92
+ tracker_id: 2
+workflows_146:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 6
+ id: 146
+ tracker_id: 2
+workflows_172:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 5
+ id: 172
+ tracker_id: 2
+workflows_254:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 3
+ id: 254
+ tracker_id: 3
+workflows_228:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 4
+ id: 228
+ tracker_id: 3
+workflows_066:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 2
+ id: 66
+ tracker_id: 1
+workflows_093:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 1
+ id: 93
+ tracker_id: 2
+workflows_147:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 6
+ id: 147
+ tracker_id: 2
+workflows_173:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 5
+ id: 173
+ tracker_id: 2
+workflows_255:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 3
+ id: 255
+ tracker_id: 3
+workflows_229:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 4
+ id: 229
+ tracker_id: 3
+workflows_067:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 2
+ id: 67
+ tracker_id: 1
+workflows_148:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 6
+ id: 148
+ tracker_id: 2
+workflows_174:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 5
+ id: 174
+ tracker_id: 2
+workflows_256:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 4
+ id: 256
+ tracker_id: 3
+workflows_068:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 2
+ id: 68
+ tracker_id: 1
+workflows_094:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 1
+ id: 94
+ tracker_id: 2
+workflows_149:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 6
+ id: 149
+ tracker_id: 2
+workflows_175:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 5
+ id: 175
+ tracker_id: 2
+workflows_257:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 4
+ id: 257
+ tracker_id: 3
+workflows_069:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 2
+ id: 69
+ tracker_id: 1
+workflows_095:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 1
+ id: 95
+ tracker_id: 2
+workflows_176:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 6
+ id: 176
+ tracker_id: 2
+workflows_258:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 4
+ id: 258
+ tracker_id: 3
+workflows_096:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 2
+ id: 96
+ tracker_id: 2
+workflows_177:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 6
+ id: 177
+ tracker_id: 2
+workflows_259:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 4
+ id: 259
+ tracker_id: 3
+workflows_097:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 2
+ id: 97
+ tracker_id: 2
+workflows_178:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 6
+ id: 178
+ tracker_id: 2
+workflows_098:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 2
+ id: 98
+ tracker_id: 2
+workflows_179:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 6
+ id: 179
+ tracker_id: 2
+workflows_099:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 2
+ id: 99
+ tracker_id: 2
+workflows_100:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 2
+ id: 100
+ tracker_id: 2
+workflows_020:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 4
+ id: 20
+ tracker_id: 1
+workflows_101:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 3
+ id: 101
+ tracker_id: 2
+workflows_021:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 5
+ id: 21
+ tracker_id: 1
+workflows_102:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 3
+ id: 102
+ tracker_id: 2
+workflows_210:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 6
+ id: 210
+ tracker_id: 3
+workflows_022:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 5
+ id: 22
+ tracker_id: 1
+workflows_103:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 3
+ id: 103
+ tracker_id: 2
+workflows_023:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 5
+ id: 23
+ tracker_id: 1
+workflows_104:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 3
+ id: 104
+ tracker_id: 2
+workflows_130:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 2
+ id: 130
+ tracker_id: 2
+workflows_211:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 1
+ id: 211
+ tracker_id: 3
+workflows_024:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 5
+ id: 24
+ tracker_id: 1
+workflows_050:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 4
+ id: 50
+ tracker_id: 1
+workflows_105:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 3
+ id: 105
+ tracker_id: 2
+workflows_131:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 3
+ id: 131
+ tracker_id: 2
+workflows_212:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 1
+ id: 212
+ tracker_id: 3
+workflows_025:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 5
+ id: 25
+ tracker_id: 1
+workflows_051:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 5
+ id: 51
+ tracker_id: 1
+workflows_106:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 4
+ id: 106
+ tracker_id: 2
+workflows_132:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 3
+ id: 132
+ tracker_id: 2
+workflows_213:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 1
+ id: 213
+ tracker_id: 3
+workflows_240:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 6
+ id: 240
+ tracker_id: 3
+workflows_026:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 6
+ id: 26
+ tracker_id: 1
+workflows_052:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 5
+ id: 52
+ tracker_id: 1
+workflows_107:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 4
+ id: 107
+ tracker_id: 2
+workflows_133:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 3
+ id: 133
+ tracker_id: 2
+workflows_214:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 1
+ id: 214
+ tracker_id: 3
+workflows_241:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 1
+ id: 241
+ tracker_id: 3
+workflows_027:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 6
+ id: 27
+ tracker_id: 1
+workflows_053:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 5
+ id: 53
+ tracker_id: 1
+workflows_080:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 4
+ id: 80
+ tracker_id: 1
+workflows_108:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 4
+ id: 108
+ tracker_id: 2
+workflows_134:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 3
+ id: 134
+ tracker_id: 2
+workflows_160:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 2
+ id: 160
+ tracker_id: 2
+workflows_215:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 1
+ id: 215
+ tracker_id: 3
+workflows_242:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 1
+ id: 242
+ tracker_id: 3
+workflows_028:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 6
+ id: 28
+ tracker_id: 1
+workflows_054:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 5
+ id: 54
+ tracker_id: 1
+workflows_081:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 5
+ id: 81
+ tracker_id: 1
+workflows_109:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 4
+ id: 109
+ tracker_id: 2
+workflows_135:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 3
+ id: 135
+ tracker_id: 2
+workflows_161:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 3
+ id: 161
+ tracker_id: 2
+workflows_216:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 2
+ id: 216
+ tracker_id: 3
+workflows_243:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 1
+ id: 243
+ tracker_id: 3
+workflows_029:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 6
+ id: 29
+ tracker_id: 1
+workflows_055:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 5
+ id: 55
+ tracker_id: 1
+workflows_082:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 5
+ id: 82
+ tracker_id: 1
+workflows_136:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 4
+ id: 136
+ tracker_id: 2
+workflows_162:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 3
+ id: 162
+ tracker_id: 2
+workflows_217:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 2
+ id: 217
+ tracker_id: 3
+workflows_270:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 6
+ id: 270
+ tracker_id: 3
+workflows_244:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 1
+ id: 244
+ tracker_id: 3
+workflows_056:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 6
+ id: 56
+ tracker_id: 1
+workflows_137:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 4
+ id: 137
+ tracker_id: 2
+workflows_163:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 3
+ id: 163
+ tracker_id: 2
+workflows_190:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 2
+ id: 190
+ tracker_id: 3
+workflows_218:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 2
+ id: 218
+ tracker_id: 3
+workflows_245:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 1
+ id: 245
+ tracker_id: 3
+workflows_057:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 6
+ id: 57
+ tracker_id: 1
+workflows_083:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 5
+ id: 83
+ tracker_id: 1
+workflows_138:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 4
+ id: 138
+ tracker_id: 2
+workflows_164:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 3
+ id: 164
+ tracker_id: 2
+workflows_191:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 3
+ id: 191
+ tracker_id: 3
+workflows_219:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 2
+ id: 219
+ tracker_id: 3
+workflows_246:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 2
+ id: 246
+ tracker_id: 3
+workflows_058:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 6
+ id: 58
+ tracker_id: 1
+workflows_084:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 5
+ id: 84
+ tracker_id: 1
+workflows_139:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 4
+ id: 139
+ tracker_id: 2
+workflows_165:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 3
+ id: 165
+ tracker_id: 2
+workflows_192:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 3
+ id: 192
+ tracker_id: 3
+workflows_247:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 2
+ id: 247
+ tracker_id: 3
+workflows_059:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 6
+ id: 59
+ tracker_id: 1
+workflows_085:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 5
+ id: 85
+ tracker_id: 1
+workflows_166:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 4
+ id: 166
+ tracker_id: 2
+workflows_248:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 2
+ id: 248
+ tracker_id: 3
+workflows_086:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 6
+ id: 86
+ tracker_id: 1
+workflows_167:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 4
+ id: 167
+ tracker_id: 2
+workflows_193:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 3
+ id: 193
+ tracker_id: 3
+workflows_249:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 2
+ id: 249
+ tracker_id: 3
+workflows_087:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 6
+ id: 87
+ tracker_id: 1
+workflows_168:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 4
+ id: 168
+ tracker_id: 2
+workflows_194:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 3
+ id: 194
+ tracker_id: 3
+workflows_088:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 6
+ id: 88
+ tracker_id: 1
+workflows_169:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 4
+ id: 169
+ tracker_id: 2
+workflows_195:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 3
+ id: 195
+ tracker_id: 3
+workflows_089:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 6
+ id: 89
+ tracker_id: 1
+workflows_196:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 4
+ id: 196
+ tracker_id: 3
+workflows_197:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 4
+ id: 197
+ tracker_id: 3
+workflows_198:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 4
+ id: 198
+ tracker_id: 3
+workflows_199:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 4
+ id: 199
+ tracker_id: 3
+workflows_010:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 2
+ id: 10
+ tracker_id: 1
+workflows_011:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 3
+ id: 11
+ tracker_id: 1
+workflows_012:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 3
+ id: 12
+ tracker_id: 1
+workflows_200:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 4
+ id: 200
+ tracker_id: 3
+workflows_013:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 3
+ id: 13
+ tracker_id: 1
+workflows_120:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 6
+ id: 120
+ tracker_id: 2
+workflows_201:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 5
+ id: 201
+ tracker_id: 3
+workflows_040:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 2
+ id: 40
+ tracker_id: 1
+workflows_121:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 1
+ id: 121
+ tracker_id: 2
+workflows_202:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 5
+ id: 202
+ tracker_id: 3
+workflows_014:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 3
+ id: 14
+ tracker_id: 1
+workflows_041:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 3
+ id: 41
+ tracker_id: 1
+workflows_122:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 1
+ id: 122
+ tracker_id: 2
+workflows_203:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 5
+ id: 203
+ tracker_id: 3
+workflows_015:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 3
+ id: 15
+ tracker_id: 1
+workflows_230:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 4
+ id: 230
+ tracker_id: 3
+workflows_123:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 1
+ id: 123
+ tracker_id: 2
+workflows_204:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 5
+ id: 204
+ tracker_id: 3
+workflows_016:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 4
+ id: 16
+ tracker_id: 1
+workflows_042:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 3
+ id: 42
+ tracker_id: 1
+workflows_231:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 5
+ id: 231
+ tracker_id: 3
+workflows_070:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 2
+ id: 70
+ tracker_id: 1
+workflows_124:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 1
+ id: 124
+ tracker_id: 2
+workflows_150:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 6
+ id: 150
+ tracker_id: 2
+workflows_205:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 5
+ id: 205
+ tracker_id: 3
+workflows_017:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 4
+ id: 17
+ tracker_id: 1
+workflows_043:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 3
+ id: 43
+ tracker_id: 1
+workflows_232:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 5
+ id: 232
+ tracker_id: 3
+workflows_125:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 1
+ id: 125
+ tracker_id: 2
+workflows_151:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 1
+ id: 151
+ tracker_id: 2
+workflows_206:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 6
+ id: 206
+ tracker_id: 3
+workflows_018:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 4
+ id: 18
+ tracker_id: 1
+workflows_044:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 3
+ id: 44
+ tracker_id: 1
+workflows_071:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 3
+ id: 71
+ tracker_id: 1
+workflows_233:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 5
+ id: 233
+ tracker_id: 3
+workflows_126:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 2
+ id: 126
+ tracker_id: 2
+workflows_152:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 1
+ id: 152
+ tracker_id: 2
+workflows_207:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 6
+ id: 207
+ tracker_id: 3
+workflows_019:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 4
+ id: 19
+ tracker_id: 1
+workflows_045:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 3
+ id: 45
+ tracker_id: 1
+workflows_260:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 4
+ id: 260
+ tracker_id: 3
+workflows_234:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 5
+ id: 234
+ tracker_id: 3
+workflows_127:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 2
+ id: 127
+ tracker_id: 2
+workflows_153:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 1
+ id: 153
+ tracker_id: 2
+workflows_180:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 6
+ id: 180
+ tracker_id: 2
+workflows_208:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 6
+ id: 208
+ tracker_id: 3
+workflows_046:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 4
+ id: 46
+ tracker_id: 1
+workflows_072:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 3
+ id: 72
+ tracker_id: 1
+workflows_261:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 5
+ id: 261
+ tracker_id: 3
+workflows_235:
+ new_status_id: 6
+ role_id: 2
+ old_status_id: 5
+ id: 235
+ tracker_id: 3
+workflows_154:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 1
+ id: 154
+ tracker_id: 2
+workflows_181:
+ new_status_id: 2
+ role_id: 1
+ old_status_id: 1
+ id: 181
+ tracker_id: 3
+workflows_209:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 6
+ id: 209
+ tracker_id: 3
+workflows_047:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 4
+ id: 47
+ tracker_id: 1
+workflows_073:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 3
+ id: 73
+ tracker_id: 1
+workflows_128:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 2
+ id: 128
+ tracker_id: 2
+workflows_262:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 5
+ id: 262
+ tracker_id: 3
+workflows_236:
+ new_status_id: 1
+ role_id: 2
+ old_status_id: 6
+ id: 236
+ tracker_id: 3
+workflows_155:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 1
+ id: 155
+ tracker_id: 2
+workflows_048:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 4
+ id: 48
+ tracker_id: 1
+workflows_074:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 3
+ id: 74
+ tracker_id: 1
+workflows_129:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 2
+ id: 129
+ tracker_id: 2
+workflows_263:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 5
+ id: 263
+ tracker_id: 3
+workflows_237:
+ new_status_id: 2
+ role_id: 2
+ old_status_id: 6
+ id: 237
+ tracker_id: 3
+workflows_182:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 1
+ id: 182
+ tracker_id: 3
+workflows_049:
+ new_status_id: 5
+ role_id: 2
+ old_status_id: 4
+ id: 49
+ tracker_id: 1
+workflows_075:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 3
+ id: 75
+ tracker_id: 1
+workflows_156:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 2
+ id: 156
+ tracker_id: 2
+workflows_264:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 5
+ id: 264
+ tracker_id: 3
+workflows_238:
+ new_status_id: 3
+ role_id: 2
+ old_status_id: 6
+ id: 238
+ tracker_id: 3
+workflows_183:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 1
+ id: 183
+ tracker_id: 3
+workflows_076:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 4
+ id: 76
+ tracker_id: 1
+workflows_157:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 2
+ id: 157
+ tracker_id: 2
+workflows_265:
+ new_status_id: 6
+ role_id: 3
+ old_status_id: 5
+ id: 265
+ tracker_id: 3
+workflows_239:
+ new_status_id: 4
+ role_id: 2
+ old_status_id: 6
+ id: 239
+ tracker_id: 3
+workflows_077:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 4
+ id: 77
+ tracker_id: 1
+workflows_158:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 2
+ id: 158
+ tracker_id: 2
+workflows_184:
+ new_status_id: 5
+ role_id: 1
+ old_status_id: 1
+ id: 184
+ tracker_id: 3
+workflows_266:
+ new_status_id: 1
+ role_id: 3
+ old_status_id: 6
+ id: 266
+ tracker_id: 3
+workflows_078:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 4
+ id: 78
+ tracker_id: 1
+workflows_159:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 2
+ id: 159
+ tracker_id: 2
+workflows_185:
+ new_status_id: 6
+ role_id: 1
+ old_status_id: 1
+ id: 185
+ tracker_id: 3
+workflows_267:
+ new_status_id: 2
+ role_id: 3
+ old_status_id: 6
+ id: 267
+ tracker_id: 3
+workflows_079:
+ new_status_id: 5
+ role_id: 3
+ old_status_id: 4
+ id: 79
+ tracker_id: 1
+workflows_186:
+ new_status_id: 1
+ role_id: 1
+ old_status_id: 2
+ id: 186
+ tracker_id: 3
+workflows_268:
+ new_status_id: 3
+ role_id: 3
+ old_status_id: 6
+ id: 268
+ tracker_id: 3
+workflows_187:
+ new_status_id: 3
+ role_id: 1
+ old_status_id: 2
+ id: 187
+ tracker_id: 3
+workflows_269:
+ new_status_id: 4
+ role_id: 3
+ old_status_id: 6
+ id: 269
+ tracker_id: 3
+workflows_188:
+ new_status_id: 4
+ role_id: 1
+ old_status_id: 2
+ id: 188
+ tracker_id: 3
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'account_controller'
+
+# Re-raise errors caught by the controller.
+class AccountController; def rescue_action(e) raise e end; end
+
+class AccountControllerTest < ActionController::TestCase
+ fixtures :users, :roles
+
+ def setup
+ @controller = AccountController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_login_should_redirect_to_back_url_param
+ # request.uri is "test.host" in test environment
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http%3A%2F%2Ftest.host%2Fissues%2Fshow%2F1'
+ assert_redirected_to '/issues/show/1'
+ end
+
+ def test_login_should_not_redirect_to_another_host
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http%3A%2F%2Ftest.foo%2Ffake'
+ assert_redirected_to '/my/page'
+ end
+
+ def test_login_with_wrong_password
+ post :login, :username => 'admin', :password => 'bad'
+ assert_response :success
+ assert_template 'login'
+ assert_tag 'div',
+ :attributes => { :class => "flash error" },
+ :content => /Invalid user or password/
+ end
+
+ if Object.const_defined?(:OpenID)
+
+ def test_login_with_openid_for_existing_user
+ Setting.self_registration = '3'
+ Setting.openid = '1'
+ 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_openid_for_existing_non_active_user
+ Setting.self_registration = '2'
+ Setting.openid = '1'
+ 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'
+ Setting.openid = '1'
+ 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'
+ Setting.openid = '1'
+ post :login, :openid_url => 'http://openid.example.com/good_user'
+ assert_redirected_to home_url
+ user = User.find_by_login('cool_user')
+ assert ! user
+ end
+
+ def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token
+ Setting.self_registration = '1'
+ Setting.openid = '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'
+ Setting.openid = '1'
+ 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'
+ Setting.openid = '1'
+ 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_setting_openid_should_return_true_when_set_to_true
+ Setting.openid = '1'
+ assert_equal true, Setting.openid?
+ end
+
+ else
+ puts "Skipping openid tests."
+ end
+
+ def test_logout
+ @request.session[:user_id] = 2
+ get :logout
+ assert_redirected_to ''
+ assert_nil @request.session[:user_id]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'admin_controller'
+
+# Re-raise errors caught by the controller.
+class AdminController; def rescue_action(e) raise e end; end
+
+class AdminControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles
+
+ def setup
+ @controller = AdminController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_no_tag :tag => 'div',
+ :attributes => { :class => /nodata/ }
+ end
+
+ def test_projects_routing
+ assert_routing(
+ {:method => :get, :path => '/admin/projects'},
+ :controller => 'admin', :action => 'projects'
+ )
+ end
+
+ def test_index_with_no_configuration_data
+ delete_configuration_data
+ get :index
+ assert_tag :tag => 'div',
+ :attributes => { :class => /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_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 IssueStatus.find_by_name('Nouveau')
+ end
+
+ def test_test_email
+ get :test_email
+ assert_redirected_to '/settings/edit?tab=notifications'
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ user = User.find(1)
+ assert_equal [user.mail], mail.bcc
+ 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_tag :td, :child => { :tag => 'span', :content => 'Foo plugin' }
+ assert_tag :td, :child => { :tag => 'span', :content => 'Bar' }
+ end
+
+ def test_info
+ get :info
+ assert_response :success
+ assert_template 'info'
+ end
+
+ private
+
+ def delete_configuration_data
+ Role.delete_all('builtin = 0')
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'application_controller'
+
+# Re-raise errors caught by the controller.
+class ApplicationController; def rescue_action(e) raise e end; end
+
+class ApplicationControllerTest < ActionController::TestCase
+ include Redmine::I18n
+
+ def setup
+ @controller = ApplicationController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # check that all language files are valid
+ def test_localization
+ lang_files_count = Dir["#{RAILS_ROOT}/config/locales/*.yml"].size
+ assert_equal lang_files_count, valid_languages.size
+ valid_languages.each do |lang|
+ assert set_language_if_valid(lang)
+ end
+ set_language_if_valid('en')
+ end
+
+ def test_call_hook_mixed_in
+ assert @controller.respond_to?(:call_hook)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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, :enabled_modules, :issues, :trackers, :attachments,
+ :versions, :wiki_pages, :wikis, :documents
+
+ def setup
+ @controller = AttachmentsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
+ User.current = nil
+ end
+
+ def test_routing
+ assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
+ assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
+ assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
+ assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
+ end
+
+ def test_recognizes
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
+ assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
+ assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
+ end
+
+ def test_show_diff
+ get :show, :id => 5
+ assert_response :success
+ assert_template 'diff'
+ assert_equal 'text/html', @response.content_type
+ end
+
+ def test_show_text_file
+ get :show, :id => 4
+ assert_response :success
+ assert_template 'file'
+ assert_equal 'text/html', @response.content_type
+ end
+
+ def test_show_text_file_should_send_if_too_big
+ Setting.file_max_size_displayed = 512
+ Attachment.find(4).update_attribute :filesize, 754.kilobyte
+
+ get :show, :id => 4
+ assert_response :success
+ assert_equal 'application/x-ruby', @response.content_type
+ end
+
+ def test_show_other
+ get :show, :id => 6
+ assert_response :success
+ assert_equal 'application/octet-stream', @response.content_type
+ end
+
+ def test_download_text_file
+ get :download, :id => 4
+ assert_response :success
+ assert_equal 'application/x-ruby', @response.content_type
+ end
+
+ def test_download_missing_file
+ get :download, :id => 2
+ assert_response 404
+ end
+
+ def test_anonymous_on_private_private
+ get :download, :id => 7
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
+ end
+
+ def test_destroy_issue_attachment
+ issue = Issue.find(3)
+ @request.session[:user_id] = 2
+
+ assert_difference 'issue.attachments.count', -1 do
+ post :destroy, :id => 1
+ end
+ # no referrer
+ assert_redirected_to 'projects/ecookbook'
+ assert_nil Attachment.find_by_id(1)
+ j = issue.journals.find(:first, :order => 'created_on DESC')
+ assert_equal 'attachment', j.details.first.property
+ assert_equal '1', j.details.first.prop_key
+ assert_equal 'error281.txt', j.details.first.old_value
+ end
+
+ def test_destroy_wiki_page_attachment
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ post :destroy, :id => 3
+ assert_response 302
+ end
+ end
+
+ def test_destroy_project_attachment
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ post :destroy, :id => 8
+ assert_response 302
+ end
+ end
+
+ def test_destroy_version_attachment
+ @request.session[:user_id] = 2
+ assert_difference 'Attachment.count', -1 do
+ post :destroy, :id => 9
+ assert_response 302
+ end
+ end
+
+ def test_destroy_without_permission
+ post :destroy, :id => 3
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdestroy%2F3'
+ assert Attachment.find_by_id(3)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'boards_controller'
+
+# Re-raise errors caught by the controller.
+class BoardsController; def rescue_action(e) raise e end; end
+
+class BoardsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
+
+ def setup
+ @controller = BoardsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/boards'},
+ :controller => 'boards', :action => 'index', :project_id => 'world_domination'
+ )
+ 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_new_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/boards/new'},
+ :controller => 'boards', :action => 'new', :project_id => 'world_domination'
+ )
+ assert_recognizes(
+ {:controller => 'boards', :action => 'new', :project_id => 'world_domination'},
+ {:method => :post, :path => '/projects/world_domination/boards'}
+ )
+ end
+
+ def test_post_new
+ @request.session[:user_id] = 2
+ assert_difference 'Board.count' do
+ post :new, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/boards/44'},
+ :controller => 'boards', :action => 'show', :id => '44', :project_id => 'world_domination'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/boards/44.atom'},
+ :controller => 'boards', :action => 'show', :id => '44', :project_id => 'world_domination', :format => 'atom'
+ )
+ 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_atom
+ get :show, :project_id => 1, :id => 1, :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed.atom'
+ assert_not_nil assigns(:board)
+ assert_not_nil assigns(:project)
+ assert_not_nil assigns(:messages)
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/boards/44/edit'},
+ :controller => 'boards', :action => 'edit', :id => '44', :project_id => 'world_domination'
+ )
+ assert_recognizes(#TODO: use PUT method to board_path, modify form accordingly
+ {:controller => 'boards', :action => 'edit', :id => '44', :project_id => 'world_domination'},
+ {:method => :post, :path => '/projects/world_domination/boards/44/edit'}
+ )
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 2
+ assert_no_difference 'Board.count' do
+ post :edit, :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_destroy_routing
+ assert_routing(#TODO: use DELETE method to board_path, modify form accoringly
+ {:method => :post, :path => '/projects/world_domination/boards/44/destroy'},
+ :controller => 'boards', :action => 'destroy', :id => '44', :project_id => 'world_domination'
+ )
+ end
+
+ def test_post_destroy
+ @request.session[:user_id] = 2
+ assert_difference 'Board.count', -1 do
+ post :destroy, :project_id => 1, :id => 2
+ end
+ assert_redirected_to '/projects/ecookbook/settings/boards'
+ assert_nil Board.find_by_id(2)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'custom_fields_controller'
+
+# Re-raise errors caught by the controller.
+class CustomFieldsController; def rescue_action(e) raise e end; end
+
+class CustomFieldsControllerTest < ActionController::TestCase
+ fixtures :custom_fields, :trackers, :users
+
+ def setup
+ @controller = CustomFieldsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @request.session[:user_id] = 1
+ end
+
+ def test_post_new_list_custom_field
+ assert_difference 'CustomField.count' do
+ post :new, :type => "IssueCustomField",
+ :custom_field => {:name => "test_post_new_list",
+ :default_value => "",
+ :min_length => "0",
+ :searchable => "0",
+ :regexp => "",
+ :is_for_all => "1",
+ :possible_values => "0.1\n0.2\n",
+ :max_length => "0",
+ :is_filter => "0",
+ :is_required =>"0",
+ :field_format => "list",
+ :tracker_ids => ["1", ""]}
+ end
+ assert_redirected_to '/custom_fields?tab=IssueCustomField'
+ field = IssueCustomField.find_by_name('test_post_new_list')
+ assert_not_nil field
+ assert_equal ["0.1", "0.2"], field.possible_values
+ assert_equal 1, field.trackers.size
+ end
+
+ def test_invalid_custom_field_class_should_redirect_to_list
+ get :new, :type => 'UnknownCustomField'
+ assert_redirected_to '/custom_fields'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'documents_controller'
+
+# Re-raise errors caught by the controller.
+class DocumentsController; def rescue_action(e) raise e end; end
+
+class DocumentsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :documents, :enumerations
+
+ def setup
+ @controller = DocumentsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/documents'},
+ :controller => 'documents', :action => 'index', :project_id => '567'
+ )
+ end
+
+ def test_index
+ # Sets a default category
+ e = Enumeration.find_by_name('Technical documentation')
+ e.update_attributes(:is_default => true)
+
+ get :index, :project_id => 'ecookbook'
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:grouped)
+
+ # Default category selected in the new document form
+ assert_tag :select, :attributes => {:name => 'document[category_id]'},
+ :child => {:tag => 'option', :attributes => {:selected => 'selected'},
+ :content => 'Technical documentation'}
+ end
+
+ def test_new_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/documents/new'},
+ :controller => 'documents', :action => 'new', :project_id => '567'
+ )
+ assert_recognizes(
+ {:controller => 'documents', :action => 'new', :project_id => '567'},
+ {:method => :post, :path => '/projects/567/documents'}
+ )
+ end
+
+ def test_new_with_one_attachment
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events << 'document_added'
+ @request.session[:user_id] = 2
+ set_tmp_attachments_directory
+
+ post :new, :project_id => 'ecookbook',
+ :document => { :title => 'DocumentsControllerTest#test_post_new',
+ :description => 'This is a new document',
+ :category_id => 2},
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+
+ assert_redirected_to 'projects/ecookbook/documents'
+
+ document = Document.find_by_title('DocumentsControllerTest#test_post_new')
+ assert_not_nil document
+ assert_equal Enumeration.find(2), document.category
+ assert_equal 1, document.attachments.size
+ assert_equal 'testfile.txt', document.attachments.first.filename
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/documents/22/edit'},
+ :controller => 'documents', :action => 'edit', :id => '22'
+ )
+ assert_recognizes(#TODO: should be using PUT on document URI
+ {:controller => 'documents', :action => 'edit', :id => '567'},
+ {:method => :post, :path => '/documents/567/edit'}
+ )
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/documents/22'},
+ :controller => 'documents', :action => 'show', :id => '22'
+ )
+ end
+
+ def test_destroy_routing
+ assert_recognizes(#TODO: should be using DELETE on document URI
+ {:controller => 'documents', :action => 'destroy', :id => '567'},
+ {:method => :post, :path => '/documents/567/destroy'}
+ )
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1
+ assert_redirected_to 'projects/ecookbook/documents'
+ assert_nil Document.find_by_id(1)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'enumerations_controller'
+
+# Re-raise errors caught by the controller.
+class EnumerationsController; def rescue_action(e) raise e end; end
+
+class EnumerationsControllerTest < ActionController::TestCase
+ fixtures :enumerations, :issues, :users
+
+ def setup
+ @controller = EnumerationsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_destroy_enumeration_not_in_use
+ post :destroy, :id => 7
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(7)
+ end
+
+ def test_destroy_enumeration_in_use
+ post :destroy, :id => 4
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil Enumeration.find_by_id(4)
+ end
+
+ def test_destroy_enumeration_in_use_with_reassignment
+ issue = Issue.find(:first, :conditions => {:priority_id => 4})
+ post :destroy, :id => 4, :reassign_to_id => 6
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(4)
+ # check that the issue was reassign
+ assert_equal 6, issue.reload.priority_id
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'groups_controller'
+
+# Re-raise errors caught by the controller.
+class GroupsController; def rescue_action(e) raise e end; end
+
+class GroupsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :groups_users
+
+ def setup
+ @controller = GroupsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_show
+ get :show, :id => 10
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_create
+ assert_difference 'Group.count' do
+ post :create, :group => {:lastname => 'New group'}
+ end
+ assert_redirected_to 'groups'
+ end
+
+ def test_edit
+ get :edit, :id => 10
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_update
+ post :update, :id => 10
+ assert_redirected_to 'groups'
+ end
+
+ def test_destroy
+ assert_difference 'Group.count', -1 do
+ post :destroy, :id => 10
+ end
+ assert_redirected_to 'groups'
+ end
+
+ def test_add_users
+ assert_difference 'Group.find(10).users.count', 2 do
+ post :add_users, :id => 10, :user_ids => ['2', '3']
+ end
+ end
+
+ def test_remove_user
+ assert_difference 'Group.find(10).users.count', -1 do
+ post :remove_user, :id => 10, :user_id => '8'
+ end
+ end
+
+ def test_new_membership
+ assert_difference 'Group.find(10).members.count' do
+ post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
+ end
+ end
+
+ def test_edit_membership
+ assert_no_difference 'Group.find(10).members.count' do
+ post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
+ end
+ end
+
+ def test_destroy_membership
+ assert_difference 'Group.find(10).members.count', -1 do
+ post :destroy_membership, :id => 10, :membership_id => 6
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = IssueCategoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 2
+ end
+
+ def test_post_edit
+ assert_no_difference 'IssueCategory.count' do
+ post :edit, :id => 2, :category => { :name => 'Testing' }
+ end
+ assert_redirected_to '/projects/ecookbook/settings/categories'
+ assert_equal 'Testing', IssueCategory.find(2).name
+ end
+
+ def test_edit_not_found
+ post :edit, :id => 97, :category => { :name => 'Testing' }
+ assert_response 404
+ end
+
+ def test_destroy_category_not_in_use
+ post :destroy, :id => 2
+ assert_redirected_to '/projects/ecookbook/settings/categories'
+ assert_nil IssueCategory.find_by_id(2)
+ end
+
+ def test_destroy_category_in_use
+ post :destroy, :id => 1
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil IssueCategory.find_by_id(1)
+ end
+
+ def test_destroy_category_in_use_with_reassignment
+ issue = Issue.find(:first, :conditions => {:category_id => 1})
+ post :destroy, :id => 1, :todo => 'reassign', :reassign_to_id => 2
+ assert_redirected_to '/projects/ecookbook/settings/categories'
+ assert_nil IssueCategory.find_by_id(1)
+ # check that the issue was reassign
+ assert_equal 2, issue.reload.category_id
+ end
+
+ def test_destroy_category_in_use_without_reassignment
+ issue = Issue.find(:first, :conditions => {:category_id => 1})
+ post :destroy, :id => 1, :todo => 'nullify'
+ assert_redirected_to '/projects/ecookbook/settings/categories'
+ assert_nil IssueCategory.find_by_id(1)
+ # check that the issue category was nullified
+ assert_nil issue.reload.category_id
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+require 'issue_relations_controller'
+
+# Re-raise errors caught by the controller.
+class IssueRelationsController; def rescue_action(e) raise e end; end
+
+
+class IssueRelationsControllerTest < ActionController::TestCase
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :issue_relations,
+ :enabled_modules,
+ :enumerations,
+ :trackers
+
+ def setup
+ @controller = IssueRelationsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_new_routing
+ assert_routing(
+ {:method => :post, :path => '/issues/1/relations'},
+ {:controller => 'issue_relations', :action => 'new', :issue_id => '1'}
+ )
+ end
+
+ def test_new
+ assert_difference 'IssueRelation.count' do
+ @request.session[:user_id] = 3
+ post :new, :issue_id => 1,
+ :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''}
+ end
+ end
+
+ def test_should_create_relations_with_visible_issues_only
+ Setting.cross_project_issue_relations = '1'
+ assert_nil Issue.visible(User.find(3)).find_by_id(4)
+
+ assert_no_difference 'IssueRelation.count' do
+ @request.session[:user_id] = 3
+ post :new, :issue_id => 1,
+ :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''}
+ end
+ end
+
+ def test_destroy_routing
+ assert_recognizes( #TODO: use DELETE on issue URI
+ {:controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23'},
+ {:method => :post, :path => '/issues/1/relations/23/destroy'}
+ )
+ end
+
+ def test_destroy
+ assert_difference 'IssueRelation.count', -1 do
+ @request.session[:user_id] = 3
+ post :destroy, :id => '2', :issue_id => '3'
+ end
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'\r
+require 'issue_statuses_controller'\r
+\r
+# Re-raise errors caught by the controller.\r
+class IssueStatusesController; def rescue_action(e) raise e end; end\r
+\r
+\r
+class IssueStatusesControllerTest < ActionController::TestCase\r
+ fixtures :issue_statuses, :issues\r
+ \r
+ def setup\r
+ @controller = IssueStatusesController.new\r
+ @request = ActionController::TestRequest.new\r
+ @response = ActionController::TestResponse.new\r
+ User.current = nil\r
+ @request.session[:user_id] = 1 # admin\r
+ end\r
+ \r
+ def test_index\r
+ # TODO: unify with #list\r
+ get :index\r
+ assert_response :success\r
+ assert_template 'list'\r
+ end\r
+ \r
+ def test_new\r
+ get :new\r
+ assert_response :success\r
+ assert_template 'new'\r
+ end\r
+ \r
+ def test_create\r
+ assert_difference 'IssueStatus.count' do\r
+ post :create, :issue_status => {:name => 'New status'}\r
+ end\r
+ assert_redirected_to 'issue_statuses/list'\r
+ status = IssueStatus.find(:first, :order => 'id DESC')\r
+ assert_equal 'New status', status.name\r
+ end\r
+ \r
+ def test_edit\r
+ get :edit, :id => '3'\r
+ assert_response :success\r
+ assert_template 'edit'\r
+ end\r
+ \r
+ def test_update\r
+ post :update, :id => '3', :issue_status => {:name => 'Renamed status'}\r
+ assert_redirected_to 'issue_statuses/list'\r
+ status = IssueStatus.find(3)\r
+ assert_equal 'Renamed status', status.name\r
+ end\r
+ \r
+ def test_destroy\r
+ Issue.delete_all("status_id = 1")\r
+ \r
+ assert_difference 'IssueStatus.count', -1 do\r
+ post :destroy, :id => '1'\r
+ end\r
+ assert_redirected_to 'issue_statuses/list'\r
+ assert_nil IssueStatus.find_by_id(1)\r
+ end\r
+ \r
+ def test_destroy_should_block_if_status_in_use\r
+ assert_not_nil Issue.find_by_status_id(1)\r
+ \r
+ assert_no_difference 'IssueStatus.count' do\r
+ post :destroy, :id => '1'\r
+ end\r
+ assert_redirected_to 'issue_statuses/list'\r
+ assert_not_nil IssueStatus.find_by_id(1)\r
+ end\r
+end\r
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'issues_controller'
+
+# Re-raise errors caught by the controller.
+class IssuesController; def rescue_action(e) raise e end; end
+
+class IssuesControllerTest < ActionController::TestCase
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :versions,
+ :trackers,
+ :projects_trackers,
+ :issue_categories,
+ :enabled_modules,
+ :enumerations,
+ :attachments,
+ :workflows,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_trackers,
+ :time_entries,
+ :journals,
+ :journal_details,
+ :queries
+
+ def setup
+ @controller = IssuesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/issues'},
+ :controller => 'issues', :action => 'index'
+ )
+ end
+
+ def test_index
+ Setting.default_language = 'en'
+
+ get :index
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:project)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ # private projects hidden
+ assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
+ assert_no_tag :tag => 'a', :content => /Issue on project 2/
+ # project column
+ assert_tag :tag => 'th', :content => /Project/
+ end
+
+ def test_index_should_not_list_issues_when_module_disabled
+ EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
+ get :index
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:project)
+ assert_no_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ end
+
+ def test_index_with_project_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/23/issues'},
+ :controller => 'issues', :action => 'index', :project_id => '23'
+ )
+ end
+
+ def test_index_should_not_list_issues_when_module_disabled
+ EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
+ get :index
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:project)
+ assert_no_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ end
+
+ def test_index_with_project_routing
+ assert_routing(
+ {:method => :get, :path => 'projects/23/issues'},
+ :controller => 'issues', :action => 'index', :project_id => '23'
+ )
+ end
+
+ def test_index_with_project
+ Setting.display_subprojects_issues = 0
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_no_tag :tag => 'a', :content => /Subproject issue/
+ end
+
+ def test_index_with_project_and_subprojects
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
+ end
+
+ def test_index_with_project_and_subprojects_should_show_private_subprojects
+ @request.session[:user_id] = 2
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ assert_tag :tag => 'a', :content => /Issue of a private subproject/
+ end
+
+ def test_index_with_project_routing_formatted
+ assert_routing(
+ {:method => :get, :path => 'projects/23/issues.pdf'},
+ :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
+ )
+ assert_routing(
+ {:method => :get, :path => 'projects/23/issues.atom'},
+ :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
+ )
+ end
+
+ def test_index_with_project_and_filter
+ get :index, :project_id => 1, :set_filter => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ end
+
+ def test_index_with_query
+ get :index, :project_id => 1, :query_id => 5
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_nil assigns(:issue_count_by_group)
+ end
+
+ def test_index_with_query_grouped_by_tracker
+ get :index, :project_id => 1, :query_id => 6
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ count_by_group = assigns(:issue_count_by_group)
+ assert_kind_of Hash, count_by_group
+ assert_kind_of Tracker, count_by_group.keys.first
+ assert_not_nil count_by_group[Tracker.find(1)]
+ end
+
+ def test_index_with_query_grouped_by_list_custom_field
+ get :index, :project_id => 1, :query_id => 9
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ count_by_group = assigns(:issue_count_by_group)
+ assert_kind_of Hash, count_by_group
+ assert_kind_of String, count_by_group.keys.first
+ assert_not_nil count_by_group['MySQL']
+ end
+
+ def test_index_csv_with_project
+ Setting.default_language = 'en'
+
+ get :index, :format => 'csv'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'text/csv', @response.content_type
+ assert @response.body.starts_with?("#,")
+
+ get :index, :project_id => 1, :format => 'csv'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'text/csv', @response.content_type
+ end
+
+ def test_index_formatted
+ assert_routing(
+ {:method => :get, :path => 'issues.pdf'},
+ :controller => 'issues', :action => 'index', :format => 'pdf'
+ )
+ assert_routing(
+ {:method => :get, :path => 'issues.atom'},
+ :controller => 'issues', :action => 'index', :format => 'atom'
+ )
+ end
+
+ def test_index_pdf
+ get :index, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+
+ get :index, :project_id => 1, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+
+ get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
+ assert_response :success
+ assert_not_nil assigns(:issues)
+ assert_equal 'application/pdf', @response.content_type
+ end
+
+ def test_index_sort
+ get :index, :sort => 'tracker,id:desc'
+ assert_response :success
+
+ sort_params = @request.session['issues_index_sort']
+ assert sort_params.is_a?(String)
+ assert_equal 'tracker,id:desc', sort_params
+
+ issues = assigns(:issues)
+ assert_not_nil issues
+ assert !issues.empty?
+ assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
+ end
+
+ def test_gantt
+ get :gantt, :project_id => 1
+ assert_response :success
+ assert_template 'gantt.rhtml'
+ assert_not_nil assigns(:gantt)
+ events = assigns(:gantt).events
+ assert_not_nil events
+ # Issue with start and due dates
+ i = Issue.find(1)
+ assert_not_nil i.due_date
+ assert events.include?(Issue.find(1))
+ # Issue with without due date but targeted to a version with date
+ i = Issue.find(2)
+ assert_nil i.due_date
+ assert events.include?(i)
+ end
+
+ def test_cross_project_gantt
+ get :gantt
+ assert_response :success
+ assert_template 'gantt.rhtml'
+ assert_not_nil assigns(:gantt)
+ events = assigns(:gantt).events
+ assert_not_nil events
+ end
+
+ def test_gantt_export_to_pdf
+ get :gantt, :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_cross_project_gantt_export_to_pdf
+ get :gantt, :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_image
+ get :gantt, :project_id => 1, :format => 'png'
+ assert_response :success
+ assert_equal 'image/png', @response.content_type
+ end
+ else
+ puts "RMagick not installed. Skipping tests !!!"
+ end
+
+ def test_calendar
+ get :calendar, :project_id => 1
+ assert_response :success
+ assert_template 'calendar'
+ assert_not_nil assigns(:calendar)
+ end
+
+ def test_cross_project_calendar
+ get :calendar
+ assert_response :success
+ assert_template 'calendar'
+ assert_not_nil assigns(:calendar)
+ end
+
+ def test_changes
+ get :changes, :project_id => 1
+ assert_response :success
+ assert_not_nil assigns(:journals)
+ assert_equal 'application/atom+xml', @response.content_type
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/issues/64'},
+ :controller => 'issues', :action => 'show', :id => '64'
+ )
+ end
+
+ def test_show_routing_formatted
+ assert_routing(
+ {:method => :get, :path => '/issues/2332.pdf'},
+ :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
+ )
+ assert_routing(
+ {:method => :get, :path => '/issues/23123.atom'},
+ :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
+ )
+ end
+
+ def test_show_by_anonymous
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show.rhtml'
+ assert_not_nil assigns(:issue)
+ assert_equal Issue.find(1), assigns(:issue)
+
+ # anonymous role is allowed to add a note
+ assert_tag :tag => 'form',
+ :descendant => { :tag => 'fieldset',
+ :child => { :tag => 'legend',
+ :content => /Notes/ } }
+ end
+
+ def test_show_by_manager
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response :success
+
+ assert_tag :tag => 'form',
+ :descendant => { :tag => 'fieldset',
+ :child => { :tag => 'legend',
+ :content => /Change properties/ } },
+ :descendant => { :tag => 'fieldset',
+ :child => { :tag => 'legend',
+ :content => /Log time/ } },
+ :descendant => { :tag => 'fieldset',
+ :child => { :tag => 'legend',
+ :content => /Notes/ } }
+ end
+
+ def test_show_should_deny_anonymous_access_without_permission
+ Role.anonymous.remove_permission!(:view_issues)
+ get :show, :id => 1
+ assert_response :redirect
+ end
+
+ def test_show_should_deny_non_member_access_without_permission
+ Role.non_member.remove_permission!(:view_issues)
+ @request.session[:user_id] = 9
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_deny_member_access_without_permission
+ Role.find(1).remove_permission!(:view_issues)
+ @request.session[:user_id] = 2
+ get :show, :id => 1
+ assert_response 403
+ end
+
+ def test_show_should_not_disclose_relations_to_invisible_issues
+ Setting.cross_project_issue_relations = '1'
+ IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
+ # Relation to a private project issue
+ IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
+
+ get :show, :id => 1
+ assert_response :success
+
+ assert_tag :div, :attributes => { :id => 'relations' },
+ :descendant => { :tag => 'a', :content => /#2$/ }
+ assert_no_tag :div, :attributes => { :id => 'relations' },
+ :descendant => { :tag => 'a', :content => /#4$/ }
+ end
+
+ def test_show_atom
+ get :show, :id => 2, :format => 'atom'
+ assert_response :success
+ assert_template 'changes.rxml'
+ # Inline image
+ assert @response.body.include?("<img src=\"http://test.host/attachments/download/10\" alt=\"\" />"), "Body did not match. Body: #{@response.body}"
+ end
+
+ def test_new_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/issues/new'},
+ :controller => 'issues', :action => 'new', :project_id => '1'
+ )
+ assert_recognizes(
+ {:controller => 'issues', :action => 'new', :project_id => '1'},
+ {:method => :post, :path => '/projects/1/issues'}
+ )
+ end
+
+ def test_show_export_to_pdf
+ get :show, :id => 3, :format => 'pdf'
+ assert_response :success
+ assert_equal 'application/pdf', @response.content_type
+ assert @response.body.starts_with?('%PDF')
+ assert_not_nil assigns(:issue)
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :tracker_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
+ :value => 'Default string' }
+ end
+
+ def test_get_new_without_tracker_id
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+
+ issue = assigns(:issue)
+ assert_not_nil issue
+ assert_equal Project.find(1).trackers.first, issue.tracker
+ end
+
+ def test_get_new_with_no_default_status_should_display_an_error
+ @request.session[:user_id] = 2
+ IssueStatus.delete_all
+
+ get :new, :project_id => 1
+ assert_response 500
+ assert_not_nil flash[:error]
+ assert_tag :tag => 'div', :attributes => { :class => /error/ },
+ :content => /No default issue/
+ end
+
+ def test_get_new_with_no_tracker_should_display_an_error
+ @request.session[:user_id] = 2
+ Tracker.delete_all
+
+ get :new, :project_id => 1
+ assert_response 500
+ assert_not_nil flash[:error]
+ assert_tag :tag => 'div', :attributes => { :class => /error/ },
+ :content => /No tracker/
+ end
+
+ def test_update_new_form
+ @request.session[:user_id] = 2
+ xhr :post, :new, :project_id => 1,
+ :issue => {:tracker_id => 2,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_new
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ issue = Issue.find_by_subject('This is the test_new issue')
+ assert_not_nil issue
+ assert_equal 2, issue.author_id
+ assert_equal 3, issue.tracker_id
+ assert_nil issue.estimated_hours
+ v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
+ assert_not_nil v
+ assert_equal 'Value for field 2', v.value
+ end
+
+ def test_post_new_and_continue
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :subject => 'This is first issue',
+ :priority_id => 5},
+ :continue => ''
+ assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
+ end
+
+ def test_post_new_without_custom_fields_param
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+ end
+
+ def test_post_new_with_required_custom_field_and_without_custom_fields_param
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:is_required, true)
+
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_template 'new'
+ issue = assigns(:issue)
+ assert_not_nil issue
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
+ end
+
+ def test_post_new_with_watchers
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ assert_difference 'Watcher.count', 2 do
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is a new issue with watchers',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :watcher_user_ids => ['2', '3']}
+ end
+ issue = Issue.find_by_subject('This is a new issue with watchers')
+ assert_not_nil issue
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+
+ # Watchers added
+ assert_equal [2, 3], issue.watcher_user_ids.sort
+ assert issue.watched_by?(User.find(3))
+ # Watchers notified
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
+ end
+
+ def test_post_new_should_send_a_notification
+ ActionMailer::Base.deliveries.clear
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count' do
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 3,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
+ end
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_post_should_preserve_fields_values_on_validation_failure
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ # empty subject
+ :subject => '',
+ :description => 'This is a description',
+ :priority_id => 6,
+ :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
+ assert_response :success
+ assert_template 'new'
+
+ assert_tag :textarea, :attributes => { :name => 'issue[description]' },
+ :content => 'This is a description'
+ assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
+ :child => { :tag => 'option', :attributes => { :selected => 'selected',
+ :value => '6' },
+ :content => 'High' }
+ # 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'}
+ end
+
+ def test_copy_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
+ :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
+ )
+ end
+
+ def test_copy_issue
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1, :copy_from => 1
+ assert_template 'new'
+ assert_not_nil assigns(:issue)
+ orig = Issue.find(1)
+ assert_equal orig.subject, assigns(:issue).subject
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/issues/1/edit'},
+ :controller => 'issues', :action => 'edit', :id => '1'
+ )
+ assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
+ {:controller => 'issues', :action => 'edit', :id => '1'},
+ {:method => :post, :path => '/issues/1/edit'}
+ )
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_not_nil assigns(:issue)
+ assert_equal Issue.find(1), assigns(:issue)
+ end
+
+ def test_get_edit_with_params
+ @request.session[:user_id] = 2
+ get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
+ assert_response :success
+ assert_template 'edit'
+
+ issue = assigns(:issue)
+ 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_equal 7, issue.priority_id
+ assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
+ :child => { :tag => 'option',
+ :content => 'Urgent',
+ :attributes => { :selected => 'selected' } }
+ end
+
+ def test_reply_routing
+ assert_routing(
+ {:method => :post, :path => '/issues/1/quoted'},
+ :controller => 'issues', :action => 'reply', :id => '1'
+ )
+ end
+
+ def test_reply_to_issue
+ @request.session[:user_id] = 2
+ get :reply, :id => 1
+ assert_response :success
+ assert_select_rjs :show, "update"
+ end
+
+ def test_reply_to_note
+ @request.session[:user_id] = 2
+ get :reply, :id => 1, :journal_id => 2
+ assert_response :success
+ assert_select_rjs :show, "update"
+ end
+
+ def test_post_edit_without_custom_fields_param
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+
+ issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
+ old_subject = issue.subject
+ new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 2) do
+ post :edit, :id => 1, :issue => {:subject => new_subject,
+ :priority_id => '6',
+ :category_id => '1' # no change
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal new_subject, issue.subject
+ # Make sure custom fields were not cleared
+ assert_equal '125', issue.custom_value_for(2).value
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
+ assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
+ end
+
+ def test_post_edit_with_custom_field_change
+ @request.session[:user_id] = 2
+ issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 3) do
+ post :edit, :id => 1, :issue => {:subject => 'Custom field change',
+ :priority_id => '6',
+ :category_id => '1', # no change
+ :custom_field_values => { '2' => 'New custom value' }
+ }
+ end
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal 'New custom value', issue.custom_value_for(2).value
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert mail.body.include?("Searchable field changed from 125 to New custom value")
+ end
+
+ def test_post_edit_with_status_and_assignee_change
+ issue = Issue.find(1)
+ assert_equal 1, issue.status_id
+ @request.session[:user_id] = 2
+ assert_difference('TimeEntry.count', 0) do
+ post :edit,
+ :id => 1,
+ :issue => { :status_id => 2, :assigned_to_id => 3 },
+ :notes => 'Assigned to dlopper',
+ :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+ issue.reload
+ assert_equal 2, issue.status_id
+ j = issue.journals.find(:first, :order => 'id DESC')
+ assert_equal 'Assigned to dlopper', j.notes
+ assert_equal 2, j.details.size
+
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.body.include?("Status changed from New to Assigned")
+ # subject should contain the new status
+ assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
+ end
+
+ def test_post_edit_with_note_only
+ notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
+ # anonymous user
+ post :edit,
+ :id => 1,
+ :notes => notes
+ assert_redirected_to :action => 'show', :id => '1'
+ j = Issue.find(1).journals.find(:first, :order => 'id DESC')
+ assert_equal notes, j.notes
+ assert_equal 0, j.details.size
+ assert_equal User.anonymous, j.user
+
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.body.include?(notes)
+ end
+
+ def test_post_edit_with_note_and_spent_time
+ @request.session[:user_id] = 2
+ spent_hours_before = Issue.find(1).spent_hours
+ assert_difference('TimeEntry.count') do
+ post :edit,
+ :id => 1,
+ :notes => '2.5 hours added',
+ :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
+ end
+ assert_redirected_to :action => 'show', :id => '1'
+
+ issue = Issue.find(1)
+
+ j = issue.journals.find(:first, :order => 'id DESC')
+ assert_equal '2.5 hours added', j.notes
+ assert_equal 0, j.details.size
+
+ t = issue.time_entries.find(:first, :order => 'id DESC')
+ assert_not_nil t
+ assert_equal 2.5, t.hours
+ assert_equal spent_hours_before + 2.5, issue.spent_hours
+ end
+
+ def test_post_edit_with_attachment_only
+ set_tmp_attachments_directory
+
+ # Delete all fixtured journals, a race condition can occur causing the wrong
+ # journal to get fetched in the next find.
+ Journal.delete_all
+
+ # anonymous user
+ post :edit,
+ :id => 1,
+ :notes => '',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ assert_redirected_to :action => 'show', :id => '1'
+ j = Issue.find(1).journals.find(:first, :order => 'id DESC')
+ assert j.notes.blank?
+ assert_equal 1, j.details.size
+ assert_equal 'testfile.txt', j.details.first.value
+ assert_equal User.anonymous, j.user
+
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.body.include?('testfile.txt')
+ end
+
+ def test_post_edit_with_no_change
+ issue = Issue.find(1)
+ issue.journals.clear
+ ActionMailer::Base.deliveries.clear
+
+ post :edit,
+ :id => 1,
+ :notes => ''
+ assert_redirected_to :action => 'show', :id => '1'
+
+ issue.reload
+ assert issue.journals.empty?
+ # No email should be sent
+ assert ActionMailer::Base.deliveries.empty?
+ end
+
+ def test_post_edit_should_send_a_notification
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ issue = Issue.find(1)
+ old_subject = issue.subject
+ new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
+
+ post :edit, :id => 1, :issue => {:subject => new_subject,
+ :priority_id => '6',
+ :category_id => '1' # no change
+ }
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_post_edit_with_invalid_spent_time
+ @request.session[:user_id] = 2
+ notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
+
+ assert_no_difference('Journal.count') do
+ post :edit,
+ :id => 1,
+ :notes => notes,
+ :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
+ end
+ assert_response :success
+ assert_template 'edit'
+
+ assert_tag :textarea, :attributes => { :name => 'notes' },
+ :content => notes
+ assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
+ end
+
+ def test_get_bulk_edit
+ @request.session[:user_id] = 2
+ get :bulk_edit, :ids => [1, 2]
+ assert_response :success
+ assert_template 'bulk_edit'
+ end
+
+ def test_bulk_edit
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_edit, :ids => [1, 2], :priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''},
+ :notes => 'Bulk editing'
+ assert_response 302
+ # check that the issues were updated
+ 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')
+ assert_equal '125', issue.custom_value_for(2).value
+ assert_equal 'Bulk editing', journal.notes
+ assert_equal 1, journal.details.size
+ end
+
+ def test_bullk_edit_should_send_a_notification
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ post(:bulk_edit,
+ {
+ :ids => [1, 2],
+ :priority_id => 7,
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => ''},
+ :notes => 'Bulk editing'
+ })
+
+ assert_response 302
+ assert_equal 2, ActionMailer::Base.deliveries.size
+ end
+
+ def test_bulk_edit_status
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_edit, :ids => [1, 2], :priority_id => '',
+ :assigned_to_id => '',
+ :status_id => '5',
+ :notes => 'Bulk editing status'
+ assert_response 302
+ issue = Issue.find(1)
+ assert issue.closed?
+ end
+
+ def test_bulk_edit_custom_field
+ @request.session[:user_id] = 2
+ # update issues priority
+ post :bulk_edit, :ids => [1, 2], :priority_id => '',
+ :assigned_to_id => '',
+ :custom_field_values => {'2' => '777'},
+ :notes => 'Bulk editing custom field'
+ assert_response 302
+
+ issue = Issue.find(1)
+ journal = issue.journals.find(:first, :order => 'created_on DESC')
+ assert_equal '777', issue.custom_value_for(2).value
+ assert_equal 1, journal.details.size
+ assert_equal '125', journal.details.first.old_value
+ assert_equal '777', journal.details.first.value
+ end
+
+ def test_bulk_unassign
+ assert_not_nil Issue.find(2).assigned_to
+ @request.session[:user_id] = 2
+ # unassign issues
+ post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
+ assert_response 302
+ # check that the issues were updated
+ assert_nil Issue.find(2).assigned_to
+ end
+
+ def test_move_routing
+ assert_routing(
+ {:method => :get, :path => '/issues/1/move'},
+ :controller => 'issues', :action => 'move', :id => '1'
+ )
+ assert_recognizes(
+ {:controller => 'issues', :action => 'move', :id => '1'},
+ {:method => :post, :path => '/issues/1/move'}
+ )
+ end
+
+ def test_move_one_issue_to_another_project
+ @request.session[:user_id] = 2
+ post :move, :id => 1, :new_project_id => 2
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_equal 2, Issue.find(1).project_id
+ end
+
+ def test_move_one_issue_to_another_project_should_follow_when_needed
+ @request.session[:user_id] = 2
+ post :move, :id => 1, :new_project_id => 2, :follow => '1'
+ assert_redirected_to '/issues/1'
+ end
+
+ def test_bulk_move_to_another_project
+ @request.session[:user_id] = 2
+ post :move, :ids => [1, 2], :new_project_id => 2
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ # Issues moved to project 2
+ assert_equal 2, Issue.find(1).project_id
+ assert_equal 2, Issue.find(2).project_id
+ # No tracker change
+ assert_equal 1, Issue.find(1).tracker_id
+ assert_equal 2, Issue.find(2).tracker_id
+ end
+
+ def test_bulk_move_to_another_tracker
+ @request.session[:user_id] = 2
+ post :move, :ids => [1, 2], :new_tracker_id => 2
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_equal 2, Issue.find(1).tracker_id
+ assert_equal 2, Issue.find(2).tracker_id
+ end
+
+ def test_bulk_copy_to_another_project
+ @request.session[:user_id] = 2
+ assert_difference 'Issue.count', 2 do
+ assert_no_difference 'Project.find(1).issues.count' do
+ post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
+ end
+ end
+ assert_redirected_to 'projects/ecookbook/issues'
+ end
+
+ def test_copy_to_another_project_should_follow_when_needed
+ @request.session[:user_id] = 2
+ post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
+ issue = Issue.first(:order => 'id DESC')
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+ end
+
+ def test_context_menu_one_issue
+ @request.session[:user_id] = 2
+ get :context_menu, :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/1/edit?issue%5Bstatus_id%5D=5',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Immediate',
+ :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&priority_id=8',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Dave Lopper',
+ :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Copy',
+ :attributes => { :href => '/projects/ecookbook/issues/1/copy',
+ :class => 'icon-copy' }
+ assert_tag :tag => 'a', :content => 'Move',
+ :attributes => { :href => '/issues/move?ids%5B%5D=1',
+ :class => 'icon-move' }
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
+ :class => 'icon-del' }
+ end
+
+ def test_context_menu_one_issue_by_anonymous
+ get :context_menu, :ids => [1]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '#',
+ :class => 'icon-del disabled' }
+ end
+
+ def test_context_menu_multiple_issues_of_same_project
+ @request.session[:user_id] = 2
+ get :context_menu, :ids => [1, 2]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_tag :tag => 'a', :content => 'Edit',
+ :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2',
+ :class => 'icon-edit' }
+ assert_tag :tag => 'a', :content => 'Immediate',
+ :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&priority_id=8',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Dave Lopper',
+ :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1&ids%5B%5D=2',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Move',
+ :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2',
+ :class => 'icon-move' }
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2',
+ :class => 'icon-del' }
+ end
+
+ def test_context_menu_multiple_issues_of_different_project
+ @request.session[:user_id] = 2
+ get :context_menu, :ids => [1, 2, 4]
+ assert_response :success
+ assert_template 'context_menu'
+ assert_tag :tag => 'a', :content => 'Delete',
+ :attributes => { :href => '#',
+ :class => 'icon-del disabled' }
+ end
+
+ def test_destroy_routing
+ assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
+ {:controller => 'issues', :action => 'destroy', :id => '1'},
+ {:method => :post, :path => '/issues/1/destroy'}
+ )
+ end
+
+ def test_destroy_issue_with_no_time_entries
+ assert_nil TimeEntry.find_by_issue_id(2)
+ @request.session[:user_id] = 2
+ post :destroy, :id => 2
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert_nil Issue.find_by_id(2)
+ end
+
+ def test_destroy_issues_with_time_entries
+ @request.session[:user_id] = 2
+ post :destroy, :ids => [1, 3]
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil assigns(:hours)
+ assert Issue.find_by_id(1) && Issue.find_by_id(3)
+ end
+
+ def test_destroy_issues_and_destroy_time_entries
+ @request.session[:user_id] = 2
+ post :destroy, :ids => [1, 3], :todo => 'destroy'
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_nil TimeEntry.find_by_id([1, 2])
+ end
+
+ def test_destroy_issues_and_assign_time_entries_to_project
+ @request.session[:user_id] = 2
+ post :destroy, :ids => [1, 3], :todo => 'nullify'
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_nil TimeEntry.find(1).issue_id
+ assert_nil TimeEntry.find(2).issue_id
+ end
+
+ def test_destroy_issues_and_reassign_time_entries_to_another_issue
+ @request.session[:user_id] = 2
+ post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
+ assert_redirected_to :action => 'index', :project_id => 'ecookbook'
+ assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
+ assert_equal 2, TimeEntry.find(1).issue_id
+ assert_equal 2, TimeEntry.find(2).issue_id
+ end
+
+ def test_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'}}}
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = JournalsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 1
+ xhr :get, :edit, :id => 2
+ assert_response :success
+ assert_select_rjs :insert, :after, 'journal-2-notes' do
+ assert_select 'form[id=journal-2-form]'
+ assert_select 'textarea'
+ end
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 1
+ xhr :post, :edit, :id => 2, :notes => 'Updated notes'
+ assert_response :success
+ assert_select_rjs :replace, 'journal-2-notes'
+ assert_equal 'Updated notes', Journal.find(2).notes
+ end
+
+ def test_post_edit_with_empty_notes
+ @request.session[:user_id] = 1
+ xhr :post, :edit, :id => 2, :notes => ''
+ assert_response :success
+ assert_select_rjs :remove, 'change-2'
+ assert_nil Journal.find_by_id(2)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'mail_handler_controller'
+
+# Re-raise errors caught by the controller.
+class MailHandlerController; def rescue_action(e) raise e end; end
+
+class MailHandlerControllerTest < ActionController::TestCase
+ fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, :trackers, :enumerations
+
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
+
+ def setup
+ @controller = MailHandlerController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_should_create_issue
+ # Enable API and set a key
+ Setting.mail_handler_api_enabled = 1
+ Setting.mail_handler_api_key = 'secret'
+
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ assert_response 201
+ end
+
+ def test_should_not_allow
+ # Disable API
+ Setting.mail_handler_api_enabled = 0
+ Setting.mail_handler_api_key = 'secret'
+
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ assert_response 403
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+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
+
+ def test_members_routing
+ assert_routing(
+ {:method => :post, :path => 'projects/5234/members/new'},
+ :controller => 'members', :action => 'new', :id => '5234'
+ )
+ end
+
+ def test_create
+ assert_difference 'Member.count' do
+ post :new, :id => 1, :member => {:role_ids => [1], :user_id => 7}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert User.find(7).member_of?(Project.find(1))
+ end
+
+ def test_create_multiple
+ assert_difference 'Member.count', 3 do
+ post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert User.find(7).member_of?(Project.find(1))
+ end
+
+ def test_edit
+ assert_no_difference 'Member.count' do
+ post :edit, :id => 2, :member => {:role_ids => [1], :user_id => 3}
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ end
+
+ def test_destroy
+ assert_difference 'Member.count', -1 do
+ post :destroy, :id => 2
+ end
+ assert_redirected_to '/projects/ecookbook/settings/members'
+ assert !User.find(3).member_of?(Project.find(1))
+ end
+
+ def test_autocomplete_for_member
+ get :autocomplete_for_member, :id => 1, :q => 'mis'
+ assert_response :success
+ assert_template 'autocomplete_for_member'
+
+ assert_tag :label, :content => /User Misc/,
+ :child => { :tag => 'input', :attributes => { :name => 'member[user_ids][]', :value => '8' } }
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'messages_controller'
+
+# Re-raise errors caught by the controller.
+class MessagesController; def rescue_action(e) raise e end; end
+
+class MessagesControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
+
+ def setup
+ @controller = MessagesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/boards/22/topics/2'},
+ :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
+ )
+ end
+
+ def test_show
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:board)
+ assert_not_nil assigns(:project)
+ assert_not_nil assigns(:topic)
+ end
+
+ def test_show_with_reply_permission
+ @request.session[:user_id] = 2
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :div, :attributes => { :id => 'reply' },
+ :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
+ end
+
+ def test_show_message_not_found
+ get :show, :board_id => 1, :id => 99999
+ assert_response 404
+ end
+
+ def test_new_routing
+ assert_routing(
+ {:method => :get, :path => '/boards/lala/topics/new'},
+ :controller => 'messages', :action => 'new', :board_id => 'lala'
+ )
+ assert_recognizes(#TODO: POST to collection, need to adjust form accordingly
+ {:controller => 'messages', :action => 'new', :board_id => 'lala'},
+ {:method => :post, :path => '/boards/lala/topics/new'}
+ )
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :board_id => 1
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_new
+ @request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events = ['message_posted']
+
+ post :new, :board_id => 1,
+ :message => { :subject => 'Test created message',
+ :content => 'Message body'}
+ message = Message.find_by_subject('Test created message')
+ assert_not_nil message
+ assert_redirected_to "boards/1/topics/#{message.to_param}"
+ assert_equal 'Message body', message.content
+ assert_equal 2, message.author_id
+ assert_equal 1, message.board_id
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject
+ assert mail.body.include?('Message body')
+ # author
+ assert mail.bcc.include?('jsmith@somenet.foo')
+ # project member
+ assert mail.bcc.include?('dlopper@somenet.foo')
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/boards/lala/topics/22/edit'},
+ :controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'
+ )
+ assert_recognizes( #TODO: use PUT to topic_path, modify form accordingly
+ {:controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'},
+ {:method => :post, :path => '/boards/lala/topics/22/edit'}
+ )
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 2
+ post :edit, :board_id => 1, :id => 1,
+ :message => { :subject => 'New subject',
+ :content => 'New body'}
+ assert_redirected_to 'boards/1/topics/1'
+ message = Message.find(1)
+ assert_equal 'New subject', message.subject
+ assert_equal 'New body', message.content
+ end
+
+ def test_reply_routing
+ assert_recognizes(
+ {:controller => 'messages', :action => 'reply', :board_id => '22', :id => '555'},
+ {:method => :post, :path => '/boards/22/topics/555/replies'}
+ )
+ end
+
+ def test_reply
+ @request.session[:user_id] = 2
+ post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
+ assert_redirected_to 'boards/1/topics/1'
+ assert Message.find_by_subject('Test reply')
+ end
+
+ def test_destroy_routing
+ assert_recognizes(#TODO: use DELETE to topic_path, adjust form accordingly
+ {:controller => 'messages', :action => 'destroy', :board_id => '22', :id => '555'},
+ {:method => :post, :path => '/boards/22/topics/555/destroy'}
+ )
+ end
+
+ def test_destroy_topic
+ @request.session[:user_id] = 2
+ post :destroy, :board_id => 1, :id => 1
+ assert_redirected_to 'projects/ecookbook/boards/1'
+ assert_nil Message.find_by_id(1)
+ end
+
+ def test_quote
+ @request.session[:user_id] = 2
+ xhr :get, :quote, :board_id => 1, :id => 3
+ assert_response :success
+ assert_select_rjs :show, 'reply'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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, :issues, :issue_statuses, :trackers, :enumerations, :custom_fields
+
+ def setup
+ @controller = MyController.new
+ @request = ActionController::TestRequest.new
+ @request.session[:user_id] = 2
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'page'
+ end
+
+ def test_page
+ get :page
+ assert_response :success
+ assert_template 'page'
+ end
+
+ def test_my_account_should_show_editable_custom_fields
+ get :account
+ assert_response :success
+ assert_template 'account'
+ assert_equal User.find(2), assigns(:user)
+
+ assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'}
+ end
+
+ def test_my_account_should_not_show_non_editable_custom_fields
+ UserCustomField.find(4).update_attribute :editable, false
+
+ get :account
+ assert_response :success
+ assert_template 'account'
+ assert_equal User.find(2), assigns(:user)
+
+ assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'}
+ end
+
+ def test_update_account
+ post :account, :user => {:firstname => "Joe",
+ :login => "root",
+ :admin => 1,
+ :custom_field_values => {"4" => "0100562500"}}
+ assert_redirected_to 'my/account'
+ user = User.find(2)
+ assert_equal user, assigns(:user)
+ assert_equal "Joe", user.firstname
+ assert_equal "jsmith", user.login
+ assert_equal "0100562500", user.custom_value_for(4).value
+ assert !user.admin?
+ end
+
+ def test_change_password
+ get :password
+ assert_response :success
+ assert_template 'password'
+
+ # non matching password confirmation
+ post :password, :password => 'jsmith',
+ :new_password => 'hello',
+ :new_password_confirmation => 'hello2'
+ assert_response :success
+ assert_template 'password'
+ assert_tag :tag => "div", :attributes => { :class => "errorExplanation" }
+
+ # wrong password
+ post :password, :password => 'wrongpassword',
+ :new_password => 'hello',
+ :new_password_confirmation => 'hello'
+ assert_response :success
+ assert_template 'password'
+ assert_equal 'Wrong password', flash[:error]
+
+ # good password
+ post :password, :password => 'jsmith',
+ :new_password => 'hello',
+ :new_password_confirmation => 'hello'
+ assert_redirected_to 'my/account'
+ assert User.try_to_login('jsmith', 'hello')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/news'},
+ :controller => 'news', :action => 'index'
+ )
+ end
+
+ def test_index_routing_formatted
+ assert_routing(
+ {:method => :get, :path => '/news.atom'},
+ :controller => 'news', :action => 'index', :format => 'atom'
+ )
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:newss)
+ assert_nil assigns(:project)
+ end
+
+ def test_index_with_project_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/news'},
+ :controller => 'news', :action => 'index', :project_id => '567'
+ )
+ end
+
+ def test_index_with_project_routing_formatted
+ assert_routing(
+ {:method => :get, :path => '/projects/567/news.atom'},
+ :controller => 'news', :action => 'index', :project_id => '567', :format => 'atom'
+ )
+ end
+
+ def test_index_with_project
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:newss)
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/news/2'},
+ :controller => 'news', :action => 'show', :id => '2'
+ )
+ end
+
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'h2', :content => /eCookbook first release/
+ end
+
+ def test_show_not_found
+ get :show, :id => 999
+ assert_response 404
+ end
+
+ def test_new_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/news/new'},
+ :controller => 'news', :action => 'new', :project_id => '567'
+ )
+ assert_recognizes(
+ {:controller => 'news', :action => 'new', :project_id => '567'},
+ {:method => :post, :path => '/projects/567/news'}
+ )
+ end
+
+ def test_get_new
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_new
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events << 'news_added'
+
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1, :news => { :title => 'NewsControllerTest',
+ :description => 'This is the description',
+ :summary => '' }
+ assert_redirected_to 'projects/ecookbook/news'
+
+ news = News.find_by_title('NewsControllerTest')
+ assert_not_nil news
+ assert_equal 'This is the description', news.description
+ assert_equal User.find(2), news.author
+ assert_equal Project.find(1), news.project
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/news/234'},
+ :controller => 'news', :action => 'show', :id => '234'
+ )
+ assert_recognizes(#TODO: PUT to news URI instead, need to modify form
+ {:controller => 'news', :action => 'edit', :id => '567'},
+ {:method => :post, :path => '/news/567/edit'}
+ )
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 2
+ post :edit, :id => 1, :news => { :description => 'Description changed by test_post_edit' }
+ assert_redirected_to 'news/1'
+ news = News.find(1)
+ assert_equal 'Description changed by test_post_edit', news.description
+ end
+
+ def test_post_new_with_validation_failure
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1, :news => { :title => '',
+ :description => 'This is the description',
+ :summary => '' }
+ assert_response :success
+ assert_template 'new'
+ assert_not_nil assigns(:news)
+ assert assigns(:news).new_record?
+ assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
+ :content => /1 error/
+ end
+
+ def test_add_comment
+ @request.session[:user_id] = 2
+ post :add_comment, :id => 1, :comment => { :comments => 'This is a NewsControllerTest comment' }
+ assert_redirected_to 'news/1'
+
+ comment = News.find(1).comments.find(:first, :order => 'created_on DESC')
+ assert_not_nil comment
+ assert_equal 'This is a NewsControllerTest comment', comment.comments
+ assert_equal User.find(2), comment.author
+ end
+
+ def test_empty_comment_should_not_be_added
+ @request.session[:user_id] = 2
+ assert_no_difference 'Comment.count' do
+ post :add_comment, :id => 1, :comment => { :comments => '' }
+ assert_response :success
+ assert_template 'show'
+ end
+ end
+
+ def test_destroy_comment
+ comments_count = News.find(1).comments.size
+ @request.session[:user_id] = 2
+ post :destroy_comment, :id => 1, :comment_id => 2
+ assert_redirected_to 'news/1'
+ assert_nil Comment.find_by_id(2)
+ assert_equal comments_count - 1, News.find(1).comments.size
+ end
+
+ def test_destroy_routing
+ assert_recognizes(#TODO: should use DELETE to news URI, need to change form
+ {:controller => 'news', :action => 'destroy', :id => '567'},
+ {:method => :post, :path => '/news/567/destroy'}
+ )
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1
+ assert_redirected_to 'projects/ecookbook/news'
+ assert_nil News.find_by_id(1)
+ end
+
+ def test_preview
+ get :preview, :project_id => 1,
+ :news => {:title => '',
+ :description => 'News description',
+ :summary => ''}
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
+ :content => /News description/
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'projects_controller'
+
+# Re-raise errors caught by the controller.
+class ProjectsController; def rescue_action(e) raise e end; end
+
+class ProjectsControllerTest < ActionController::TestCase
+ fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
+ :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
+ :attachments, :custom_fields, :custom_values, :time_entries
+
+ def setup
+ @controller = ProjectsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @request.session[:user_id] = nil
+ Setting.default_language = 'en'
+ end
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/projects'},
+ :controller => 'projects', :action => 'index'
+ )
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:projects)
+
+ assert_tag :ul, :child => {:tag => 'li',
+ :descendant => {:tag => 'a', :content => 'eCookbook'},
+ :child => { :tag => 'ul',
+ :descendant => { :tag => 'a',
+ :content => 'Child of private child'
+ }
+ }
+ }
+
+ assert_no_tag :a, :content => /Private child of eCookbook/
+ end
+
+ def test_index_atom_routing
+ assert_routing(
+ {:method => :get, :path => '/projects.atom'},
+ :controller => 'projects', :action => 'index', :format => 'atom'
+ )
+ end
+
+ def test_index_atom
+ get :index, :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed.atom.rxml'
+ assert_select 'feed>title', :text => 'Redmine: Latest projects'
+ assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
+ end
+
+ def test_add_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/new'},
+ :controller => 'projects', :action => 'add'
+ )
+ assert_recognizes(
+ {:controller => 'projects', :action => 'add'},
+ {:method => :post, :path => '/projects/new'}
+ )
+ assert_recognizes(
+ {:controller => 'projects', :action => 'add'},
+ {:method => :post, :path => '/projects'}
+ )
+ end
+
+ def test_get_add
+ @request.session[:user_id] = 1
+ get :add
+ assert_response :success
+ assert_template 'add'
+ end
+
+ def test_get_add_by_non_admin
+ @request.session[:user_id] = 2
+ get :add
+ assert_response :success
+ assert_template 'add'
+ end
+
+ def test_post_add
+ @request.session[:user_id] = 1
+ post :add, :project => { :name => "blog",
+ :description => "weblog",
+ :identifier => "blog",
+ :is_public => 1,
+ :custom_field_values => { '3' => 'Beta' }
+ }
+ 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_nil project.parent
+ end
+
+ def test_post_add_subproject
+ @request.session[:user_id] = 1
+ post :add, :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
+
+ def test_post_add_by_non_admin
+ @request.session[:user_id] = 2
+ post :add, :project => { :name => "blog",
+ :description => "weblog",
+ :identifier => "blog",
+ :is_public => 1,
+ :custom_field_values => { '3' => 'Beta' }
+ }
+ 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?
+
+ # User should be added as a project member
+ assert User.find(2).member_of?(project)
+ assert_equal 1, project.members.size
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/test'},
+ :controller => 'projects', :action => 'show', :id => 'test'
+ )
+ end
+
+ def test_show_by_id
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:project)
+ end
+
+ def test_show_by_identifier
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:project)
+ assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
+ end
+
+ def test_show_should_not_fail_when_custom_values_are_nil
+ project = Project.find_by_identifier('ecookbook')
+ project.custom_values.first.update_attribute(:value, nil)
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:project)
+ assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
+ end
+
+ def test_private_subprojects_hidden
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_no_tag :tag => 'a', :content => /Private child/
+ end
+
+ def test_private_subprojects_visible
+ @request.session[:user_id] = 2 # manager who is a member of the private subproject
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'a', :content => /Private child/
+ end
+
+ def test_settings_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/4223/settings'},
+ :controller => 'projects', :action => 'settings', :id => '4223'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/4223/settings/members'},
+ :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
+ )
+ end
+
+ def test_settings
+ @request.session[:user_id] = 2 # manager
+ get :settings, :id => 1
+ assert_response :success
+ assert_template 'settings'
+ end
+
+ def test_edit
+ @request.session[:user_id] = 2 # manager
+ post :edit, :id => 1, :project => {:name => 'Test changed name',
+ :issue_custom_field_ids => ['']}
+ assert_redirected_to 'projects/ecookbook/settings'
+ project = Project.find(1)
+ assert_equal 'Test changed name', project.name
+ end
+
+ def test_add_version_routing
+ assert_routing(
+ {:method => :get, :path => 'projects/64/versions/new'},
+ :controller => 'projects', :action => 'add_version', :id => '64'
+ )
+ assert_routing(
+ #TODO: use PUT
+ {:method => :post, :path => 'projects/64/versions/new'},
+ :controller => 'projects', :action => 'add_version', :id => '64'
+ )
+ end
+
+ def test_add_issue_category_routing
+ assert_routing(
+ {:method => :get, :path => 'projects/test/categories/new'},
+ :controller => 'projects', :action => 'add_issue_category', :id => 'test'
+ )
+ assert_routing(
+ #TODO: use PUT and update form
+ {:method => :post, :path => 'projects/64/categories/new'},
+ :controller => 'projects', :action => 'add_issue_category', :id => '64'
+ )
+ end
+
+ def test_destroy_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/destroy'},
+ :controller => 'projects', :action => 'destroy', :id => '567'
+ )
+ assert_routing(
+ #TODO: use DELETE and update form
+ {:method => :post, :path => 'projects/64/destroy'},
+ :controller => 'projects', :action => 'destroy', :id => '64'
+ )
+ end
+
+ def test_get_destroy
+ @request.session[:user_id] = 1 # admin
+ get :destroy, :id => 1
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil Project.find_by_id(1)
+ end
+
+ def test_post_destroy
+ @request.session[:user_id] = 1 # admin
+ post :destroy, :id => 1, :confirm => 1
+ assert_redirected_to 'admin/projects'
+ assert_nil Project.find_by_id(1)
+ end
+
+ def test_add_file
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ Setting.notified_events = ['file_added']
+ ActionMailer::Base.deliveries.clear
+
+ assert_difference 'Attachment.count' do
+ post :add_file, :id => 1, :version_id => '',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ assert_redirected_to 'projects/ecookbook/files'
+ a = Attachment.find(:first, :order => 'created_on DESC')
+ assert_equal 'testfile.txt', a.filename
+ assert_equal Project.find(1), a.container
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert_equal "[eCookbook] New file", mail.subject
+ assert mail.body.include?('testfile.txt')
+ end
+
+ def test_add_file_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/33/files/new'},
+ :controller => 'projects', :action => 'add_file', :id => '33'
+ )
+ assert_routing(
+ {:method => :post, :path => '/projects/33/files/new'},
+ :controller => 'projects', :action => 'add_file', :id => '33'
+ )
+ end
+
+ def test_add_version_file
+ set_tmp_attachments_directory
+ @request.session[:user_id] = 2
+ Setting.notified_events = ['file_added']
+
+ assert_difference 'Attachment.count' do
+ post :add_file, :id => 1, :version_id => '2',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
+ end
+ assert_redirected_to 'projects/ecookbook/files'
+ a = Attachment.find(:first, :order => 'created_on DESC')
+ assert_equal 'testfile.txt', a.filename
+ assert_equal Version.find(2), a.container
+ end
+
+ def test_list_files
+ get :list_files, :id => 1
+ assert_response :success
+ assert_template 'list_files'
+ assert_not_nil assigns(:containers)
+
+ # file attached to the project
+ assert_tag :a, :content => 'project_file.zip',
+ :attributes => { :href => '/attachments/download/8/project_file.zip' }
+
+ # file attached to a project's version
+ assert_tag :a, :content => 'version_file.zip',
+ :attributes => { :href => '/attachments/download/9/version_file.zip' }
+ end
+
+ def test_list_files_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/33/files'},
+ :controller => 'projects', :action => 'list_files', :id => '33'
+ )
+ end
+
+ def test_changelog_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/44/changelog'},
+ :controller => 'projects', :action => 'changelog', :id => '44'
+ )
+ end
+
+ def test_changelog
+ get :changelog, :id => 1
+ assert_response :success
+ assert_template 'changelog'
+ assert_not_nil assigns(:versions)
+ end
+
+ def test_roadmap_routing
+ assert_routing(
+ {:method => :get, :path => 'projects/33/roadmap'},
+ :controller => 'projects', :action => 'roadmap', :id => '33'
+ )
+ end
+
+ def test_roadmap
+ get :roadmap, :id => 1
+ assert_response :success
+ assert_template 'roadmap'
+ assert_not_nil assigns(:versions)
+ # Version with no date set appears
+ assert assigns(:versions).include?(Version.find(3))
+ # Completed version doesn't appear
+ assert !assigns(:versions).include?(Version.find(1))
+ end
+
+ def test_roadmap_with_completed_versions
+ get :roadmap, :id => 1, :completed => 1
+ assert_response :success
+ assert_template 'roadmap'
+ assert_not_nil assigns(:versions)
+ # Version with no date set appears
+ assert assigns(:versions).include?(Version.find(3))
+ # Completed version appears
+ assert assigns(:versions).include?(Version.find(1))
+ end
+
+ def test_project_activity_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/activity'},
+ :controller => 'projects', :action => 'activity', :id => '1'
+ )
+ end
+
+ def test_project_activity_atom_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/activity.atom'},
+ :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
+ )
+ end
+
+ def test_project_activity
+ get :activity, :id => 1, :with_subprojects => 0
+ assert_response :success
+ assert_template 'activity'
+ 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})/,
+ }
+ }
+ }
+ end
+
+ def test_previous_project_activity
+ get :activity, :id => 1, :from => 3.days.ago.to_date
+ assert_response :success
+ assert_template 'activity'
+ 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 => /#{Issue.find(1).subject}/,
+ }
+ }
+ }
+ end
+
+ def test_global_activity_routing
+ assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
+ end
+
+ def test_global_activity
+ get :activity
+ assert_response :success
+ assert_template 'activity'
+ assert_not_nil assigns(:events_by_day)
+
+ assert_tag :tag => "h3",
+ :content => /#{5.day.ago.to_date.day}/,
+ :sibling => { :tag => "dl",
+ :child => { :tag => "dt",
+ :attributes => { :class => /issue/ },
+ :child => { :tag => "a",
+ :content => /#{Issue.find(5).subject}/,
+ }
+ }
+ }
+ end
+
+ def test_user_activity
+ get :activity, :user_id => 2
+ assert_response :success
+ assert_template 'activity'
+ 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 => /#{Issue.find(1).subject}/,
+ }
+ }
+ }
+ end
+
+ def test_global_activity_atom_routing
+ assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
+ end
+
+ def test_activity_atom_feed
+ get :activity, :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed.atom.rxml'
+ end
+
+ def test_archive_routing
+ assert_routing(
+ #TODO: use PUT to project path and modify form
+ {:method => :post, :path => 'projects/64/archive'},
+ :controller => 'projects', :action => 'archive', :id => '64'
+ )
+ end
+
+ def test_archive
+ @request.session[:user_id] = 1 # admin
+ post :archive, :id => 1
+ assert_redirected_to 'admin/projects'
+ assert !Project.find(1).active?
+ end
+
+ def test_unarchive_routing
+ assert_routing(
+ #TODO: use PUT to project path and modify form
+ {:method => :post, :path => '/projects/567/unarchive'},
+ :controller => 'projects', :action => 'unarchive', :id => '567'
+ )
+ end
+
+ def test_unarchive
+ @request.session[:user_id] = 1 # admin
+ Project.find(1).archive
+ post :unarchive, :id => 1
+ assert_redirected_to 'admin/projects'
+ assert Project.find(1).active?
+ end
+
+ def test_project_breadcrumbs_should_be_limited_to_3_ancestors
+ CustomField.delete_all
+ parent = nil
+ 6.times do |i|
+ p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
+ p.set_parent!(parent)
+ get :show, :id => p
+ assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
+ :children => { :count => [i, 3].min,
+ :only => { :tag => 'a' } }
+
+ parent = p
+ end
+ end
+
+ def test_copy_with_project
+ @request.session[:user_id] = 1 # admin
+ get :copy, :id => 1
+ assert_response :success
+ assert_template 'copy'
+ assert assigns(:project)
+ assert_equal Project.find(1).description, assigns(:project).description
+ assert_nil assigns(:project).id
+ end
+
+ def test_copy_without_project
+ @request.session[:user_id] = 1 # admin
+ get :copy
+ assert_response :redirect
+ assert_redirected_to :controller => 'admin', :action => 'projects'
+ end
+
+ def test_jump_should_redirect_to_active_tab
+ get :show, :id => 1, :jump => 'issues'
+ assert_redirected_to 'projects/ecookbook/issues'
+ end
+
+ def test_jump_should_not_redirect_to_inactive_tab
+ get :show, :id => 3, :jump => 'documents'
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_jump_should_not_redirect_to_unknown_tab
+ get :show, :id => 3, :jump => 'foobar'
+ assert_response :success
+ assert_template 'show'
+ end
+
+ def test_reset_activities_routing
+ assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
+ :controller => 'projects', :action => 'reset_activities', :id => '64')
+ end
+
+ def test_reset_activities
+ @request.session[:user_id] = 2 # manager
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.find(:first),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.find(:last),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+ delete :reset_activities, :id => 1
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ assert_nil TimeEntryActivity.find_by_id(project_activity.id)
+ assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
+ end
+
+ def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
+ @request.session[:user_id] = 2 # manager
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific Design',
+ :parent => TimeEntryActivity.find(9),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
+ assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
+
+ delete :reset_activities, :id => 1
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ assert_nil TimeEntryActivity.find_by_id(project_activity.id)
+ assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
+ end
+
+ def test_save_activities_routing
+ assert_routing({:method => :post, :path => 'projects/64/activities/save'},
+ :controller => 'projects', :action => 'save_activities', :id => '64')
+ end
+
+ def test_save_activities_to_override_system_activities
+ @request.session[:user_id] = 2 # manager
+ billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
+
+ post :save_activities, :id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
+ "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
+ "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
+ "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
+ }
+
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+
+ # ... Design
+ design = project.time_entry_activities.find_by_name("Design")
+ assert design, "Project activity not found"
+
+ assert_equal 9, design.parent_id # Relate to the system activity
+ assert_not_equal design.parent.id, design.id # Different records
+ assert_equal design.parent.name, design.name # Same name
+ assert !design.active?
+
+ # ... Development
+ development = project.time_entry_activities.find_by_name("Development")
+ assert development, "Project activity not found"
+
+ assert_equal 10, development.parent_id # Relate to the system activity
+ assert_not_equal development.parent.id, development.id # Different records
+ assert_equal development.parent.name, development.name # Same name
+ assert development.active?
+ assert_equal "0", development.custom_value_for(billable_field).value
+
+ # ... Inactive Activity
+ previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
+ assert previously_inactive, "Project activity not found"
+
+ assert_equal 14, previously_inactive.parent_id # Relate to the system activity
+ assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
+ assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
+ assert previously_inactive.active?
+ assert_equal "1", previously_inactive.custom_value_for(billable_field).value
+
+ # ... QA
+ assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
+ end
+
+ def test_save_activities_will_update_project_specific_activities
+ @request.session[:user_id] = 2 # manager
+
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.find(:first),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.find(:last),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+
+ post :save_activities, :id => 1, :enumerations => {
+ project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
+ project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
+ }
+
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+ assert_equal 2, project.time_entry_activities.count
+
+ activity_one = project.time_entry_activities.find_by_name(project_activity.name)
+ assert activity_one, "Project activity not found"
+ assert_equal project_activity.id, activity_one.id
+ assert !activity_one.active?
+
+ activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
+ assert activity_two, "Project activity not found"
+ assert_equal project_activity_two.id, activity_two.id
+ assert !activity_two.active?
+ end
+
+ def test_save_activities_when_creating_new_activities_will_convert_existing_data
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
+
+ @request.session[:user_id] = 2 # manager
+ post :save_activities, :id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
+ }
+ assert_response :redirect
+
+ # No more TimeEntries using the system activity
+ assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
+ # All TimeEntries using project activity
+ project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
+ end
+
+ def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
+ # TODO: Need to cause an exception on create but these tests
+ # aren't setup for mocking. Just create a record now so the
+ # second one is a dupicate
+ parent = TimeEntryActivity.find(9)
+ TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
+ TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
+
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
+ assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
+
+ @request.session[:user_id] = 2 # manager
+ post :save_activities, :id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
+ "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
+ }
+ assert_response :redirect
+
+ # TimeEntries shouldn't have been reassigned on the failed record
+ assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
+ # TimeEntries shouldn't have been reassigned on the saved record either
+ assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
+ end
+
+ # A hook that is manually registered later
+ class ProjectBasedTemplate < Redmine::Hook::ViewListener
+ def view_layouts_base_html_head(context)
+ # Adds a project stylesheet
+ stylesheet_link_tag(context[:project].identifier) if context[:project]
+ end
+ end
+ # Don't use this hook now
+ Redmine::Hook.clear_listeners
+
+ def test_hook_response
+ Redmine::Hook.add_listener(ProjectBasedTemplate)
+ get :show, :id => 1
+ assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
+ :parent => {:tag => 'head'}
+
+ Redmine::Hook.clear_listeners
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'queries_controller'
+
+# Re-raise errors caught by the controller.
+class QueriesController; def rescue_action(e) raise e end; end
+
+class QueriesControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
+
+ def setup
+ @controller = QueriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_get_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_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ end
+
+ def test_get_new_global_query
+ @request.session[:user_id] = 2
+ get :new
+ assert_response :success
+ assert_template 'new'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => nil }
+ end
+
+ def test_new_project_public_query
+ @request.session[:user_id] = 2
+ post :new,
+ :project_id => 'ecookbook',
+ :confirm => '1',
+ :default_columns => '1',
+ :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_public_query", "is_public" => "1"}
+
+ q = Query.find_by_name('test_new_project_public_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_new_project_private_query
+ @request.session[:user_id] = 3
+ post :new,
+ :project_id => 'ecookbook',
+ :confirm => '1',
+ :default_columns => '1',
+ :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"}
+
+ q = Query.find_by_name('test_new_project_private_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_new_global_private_query_with_custom_columns
+ @request.session[:user_id] = 3
+ post :new,
+ :confirm => '1',
+ :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", "column_names" => ["", "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 q.valid?
+ end
+
+ def test_new_with_sort
+ @request.session[:user_id] = 1
+ post :new,
+ :confirm => '1',
+ :default_columns => '1',
+ :operators => {"status_id" => "o"},
+ :values => {"status_id" => ["1"]},
+ :query => {:name => "test_new_with_sort",
+ :is_public => "1",
+ :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
+
+ query = Query.find_by_name("test_new_with_sort")
+ assert_not_nil query
+ assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
+ end
+
+ def test_get_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_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_global_public_query
+ @request.session[:user_id] = 1
+ post :edit,
+ :id => 4,
+ :confirm => '1',
+ :default_columns => '1',
+ :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"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
+ q = Query.find_by_name('test_edit_global_public_query')
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_get_edit_global_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 3
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_global_private_query
+ @request.session[:user_id] = 3
+ post :edit,
+ :id => 3,
+ :confirm => '1',
+ :default_columns => '1',
+ :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"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
+ q = Query.find_by_name('test_edit_global_private_query')
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_get_edit_project_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ end
+
+ def test_get_edit_project_public_query
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => 'checked'
+ }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => 'disabled' }
+ end
+
+ def test_get_edit_sort_criteria
+ @request.session[:user_id] = 1
+ get :edit, :id => 5
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+ :child => { :tag => 'option', :attributes => { :value => 'priority',
+ :selected => 'selected' } }
+ assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
+ :child => { :tag => 'option', :attributes => { :value => 'desc',
+ :selected => 'selected' } }
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
+ assert_nil Query.find_by_id(1)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'reports_controller'
+
+# Re-raise errors caught by the controller.
+class ReportsController; def rescue_action(e) raise e end; end
+
+
+class ReportsControllerTest < ActionController::TestCase
+ fixtures :all
+
+ def setup
+ @controller = ReportsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_issue_report_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/issues/report'},
+ :controller => 'reports', :action => 'issue_report', :id => '567'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/issues/report/assigned_to'},
+ :controller => 'reports', :action => 'issue_report', :id => '567', :detail => 'assigned_to'
+ )
+
+ end
+
+ def test_issue_report
+ get :issue_report, :id => 1
+ assert_response :success
+ assert_template 'issue_report'
+ end
+
+ def test_issue_report_details
+ %w(tracker version priority category assigned_to author subproject).each do |detail|
+ get :issue_report, :id => 1, :detail => detail
+ assert_response :success
+ assert_template 'issue_report_details'
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesBazaarControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ Repository::Bazaar.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 2, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ get :show, :id => 3, :path => ['directory']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'directory/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ get :show, :id => 3, :path => [], :rev => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => 3, :path => ['doc-mkdir.txt']
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'doc-mkdir.txt'
+ end
+
+ def test_entry_show
+ get :entry, :id => 3, :path => ['directory', 'doc-ls.txt']
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => /29/,
+ :attributes => { :class => /line-num/ },
+ :sibling => { :tag => 'td', :content => /Show help message/ }
+ end
+
+ def test_entry_download
+ get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'], :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('Show help message')
+ end
+
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['directory']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'directory', assigns(:entry).name
+ end
+
+ def test_diff
+ # Full diff of changeset 3
+ get :diff, :id => 3, :rev => 3
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /2/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_in/ },
+ :content => /Main purpose/ }
+ end
+
+ def test_annotate
+ get :annotate, :id => 3, :path => ['doc-mkdir.txt']
+ assert_response :success
+ assert_template 'annotate'
+ # Line 2, revision 3
+ assert_tag :tag => 'th', :content => /2/,
+ :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /3/ } },
+ :sibling => { :tag => 'td', :content => /jsmith/ },
+ :sibling => { :tag => 'td', :content => /Main purpose/ }
+ end
+ else
+ puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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, :repositories, :issues, :issue_statuses, :changesets, :changes, :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
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/redmine/repository'},
+ :controller => 'repositories', :action => 'show', :id => 'redmine'
+ )
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/world_domination/repository/edit'},
+ :controller => 'repositories', :action => 'edit', :id => 'world_domination'
+ )
+ assert_routing(
+ {:method => :post, :path => '/projects/world_domination/repository/edit'},
+ :controller => 'repositories', :action => 'edit', :id => 'world_domination'
+ )
+ end
+
+ def test_revisions_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/redmine/repository/revisions'},
+ :controller => 'repositories', :action => 'revisions', :id => 'redmine'
+ )
+ end
+
+ def test_revisions_atom_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/redmine/repository/revisions.atom'},
+ :controller => 'repositories', :action => 'revisions', :id => 'redmine', :format => 'atom'
+ )
+ end
+
+ def test_revisions
+ get :revisions, :id => 1
+ assert_response :success
+ assert_template 'revisions'
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_revision_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2457'},
+ :controller => 'repositories', :action => 'revision', :id => 'restmine', :rev => '2457'
+ )
+ end
+
+ def test_revision
+ get :revision, :id => 1, :rev => 1
+ assert_response :success
+ assert_not_nil assigns(:changeset)
+ assert_equal "1", assigns(:changeset).revision
+ end
+
+ def test_revision_with_before_nil_and_afer_normal
+ get :revision, {:id => 1, :rev => 1}
+ assert_response :success
+ assert_template 'revision'
+ assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
+ :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
+ }
+ assert_tag :tag => "div", :attributes => { :class => "contextual" },
+ :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
+ }
+ end
+
+ def test_diff_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2457/diff'},
+ :controller => 'repositories', :action => 'diff', :id => 'restmine', :rev => '2457'
+ )
+ end
+
+ def test_unified_diff_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2457/diff.diff'},
+ :controller => 'repositories', :action => 'diff', :id => 'restmine', :rev => '2457', :format => 'diff'
+ )
+ end
+
+ def test_diff_path_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/diff/path/to/file.c'},
+ :controller => 'repositories', :action => 'diff', :id => 'restmine', :path => %w[path to file.c]
+ )
+ end
+
+ def test_diff_path_routing_with_revision
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2/diff/path/to/file.c'},
+ :controller => 'repositories', :action => 'diff', :id => 'restmine', :path => %w[path to file.c], :rev => '2'
+ )
+ end
+
+ def test_browse_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/browse/path/to/dir'},
+ :controller => 'repositories', :action => 'browse', :id => 'restmine', :path => %w[path to dir]
+ )
+ end
+
+ def test_entry_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/entry/path/to/file.c'},
+ :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c]
+ )
+ end
+
+ def test_entry_routing_with_revision
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2/entry/path/to/file.c'},
+ :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c], :rev => '2'
+ )
+ end
+
+ def test_raw_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/raw/path/to/file.c'},
+ :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c], :format => 'raw'
+ )
+ end
+
+ def test_raw_routing_with_revision
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/revisions/2/raw/path/to/file.c'},
+ :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c], :format => 'raw', :rev => '2'
+ )
+ end
+
+ def test_annotate_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/annotate/path/to/file.c'},
+ :controller => 'repositories', :action => 'annotate', :id => 'restmine', :path => %w[path to file.c]
+ )
+ end
+
+ def test_changesrouting
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/changes/path/to/file.c'},
+ :controller => 'repositories', :action => 'changes', :id => 'restmine', :path => %w[path to file.c]
+ )
+ end
+
+ def test_statistics_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/restmine/repository/statistics'},
+ :controller => 'repositories', :action => 'stats', :id => 'restmine'
+ )
+ end
+
+ def test_graph_commits_per_month
+ get :graph, :id => 1, :graph => 'commits_per_month'
+ assert_response :success
+ assert_equal 'image/svg+xml', @response.content_type
+ end
+
+ def test_graph_commits_per_author
+ get :graph, :id => 1, :graph => 'commits_per_author'
+ assert_response :success
+ assert_equal 'image/svg+xml', @response.content_type
+ end
+
+ def test_committers
+ @request.session[:user_id] = 2
+ # add a commit with an unknown user
+ Changeset.create!(:repository => Project.find(1).repository, :committer => 'foo', :committed_on => Time.now, :revision => 100, :comments => 'Committed by foo.')
+
+ get :committers, :id => 1
+ assert_response :success
+ assert_template 'committers'
+
+ assert_tag :td, :content => 'dlopper',
+ :sibling => { :tag => 'td',
+ :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
+ :child => { :tag => 'option', :content => 'Dave Lopper',
+ :attributes => { :value => '3', :selected => 'selected' }}}}
+ assert_tag :td, :content => 'foo',
+ :sibling => { :tag => 'td',
+ :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
+ assert_no_tag :td, :content => 'foo',
+ :sibling => { :tag => 'td',
+ :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
+ end
+
+ def test_map_committers
+ @request.session[:user_id] = 2
+ # add a commit with an unknown user
+ c = Changeset.create!(:repository => Project.find(1).repository, :committer => 'foo', :committed_on => Time.now, :revision => 100, :comments => 'Committed by foo.')
+
+ assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
+ post :committers, :id => 1, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
+ assert_redirected_to 'projects/ecookbook/repository/committers'
+ assert_equal User.find(2), c.reload.user
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesCvsControllerTest < ActionController::TestCase
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+ # CVS module
+ MODULE_NAME = 'test'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ Setting.default_language = 'en'
+ User.current = nil
+
+ @project = Project.find(1)
+ @project.repository = Repository::Cvs.create(:root_url => REPOSITORY_PATH,
+ :url => MODULE_NAME)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+
+ entry = assigns(:entries).detect {|e| e.name == 'images'}
+ assert_equal 'dir', entry.kind
+
+ entry = assigns(:entries).detect {|e| e.name == 'README'}
+ assert_equal 'file', entry.kind
+ end
+
+ def test_browse_directory
+ get :show, :id => 1, :path => ['images']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ Project.find(1).repository.fetch_changesets
+ get :show, :id => 1, :path => ['images'], :rev => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_entry
+ get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'entry'
+ assert_no_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /before_filter/
+ end
+
+ def test_entry_at_given_revision
+ # changesets must be loaded
+ Project.find(1).repository.fetch_changesets
+ get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3
+ assert_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /before_filter/
+ end
+
+ def test_entry_not_found
+ get :entry, :id => 1, :path => ['sources', 'zzz.c']
+ assert_tag :tag => 'div', :attributes => { :class => /error/ },
+ :content => /The entry or revision was not found in the repository/
+ end
+
+ def test_entry_download
+ get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
+ assert_response :success
+ end
+
+ def test_directory_entry
+ get :entry, :id => 1, :path => ['sources']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ Project.find(1).repository.fetch_changesets
+ get :diff, :id => 1, :rev => 3, :type => 'inline'
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' },
+ :content => /watched.remove_watcher/
+ assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
+ :content => /watched.remove_all_watcher/
+ end
+
+ def test_annotate
+ Project.find(1).repository.fetch_changesets
+ get :annotate, :id => 1, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'annotate'
+ # 1.1 line
+ assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
+ :content => '18',
+ :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
+ :content => /1.1/,
+ :sibling => { :tag => 'td', :attributes => { :class => 'author' },
+ :content => /LANG/
+ }
+ }
+ # 1.2 line
+ assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
+ :content => '32',
+ :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
+ :content => /1.2/,
+ :sibling => { :tag => 'td', :attributes => { :class => 'author' },
+ :content => /LANG/
+ }
+ }
+ end
+ else
+ puts "CVS test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesDarcsControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ Repository::Darcs.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ get :show, :id => 3, :path => ['images']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ Project.find(3).repository.fetch_changesets
+ get :show, :id => 3, :path => ['images'], :rev => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => 3, :path => ['images', 'edit.png']
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_diff
+ Project.find(3).repository.fetch_changesets
+ # Full diff of changeset 5
+ get :diff, :id => 3, :rev => 5
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /22/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ end
+ else
+ puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesGitControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ Repository::Git.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 6, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'copied_README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'new_file.txt' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'renamed_test.txt' && e.kind == 'file'}
+ end
+
+ def test_browse_branch
+ get :show, :id => 3, :rev => 'test_branch'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 4, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ assert assigns(:entries).detect {|e| e.name == 'test.txt' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ get :show, :id => 3, :path => ['images']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ get :show, :id => 3, :path => ['images'], :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518'
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => 3, :path => ['images', 'edit.png']
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_entry_show
+ get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => /11/,
+ :attributes => { :class => /line-num/ },
+ :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
+ end
+
+ def test_entry_download
+ get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('WITHOUT ANY WARRANTY')
+ end
+
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['sources']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ # Full diff of changeset 2f9c0091
+ get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /22/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ end
+
+ def test_annotate
+ get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'annotate'
+ # Line 23, changeset 2f9c0091
+ assert_tag :tag => 'th', :content => /24/,
+ :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /2f9c0091/ } },
+ :sibling => { :tag => 'td', :content => /jsmith/ },
+ :sibling => { :tag => 'td', :content => /watcher =/ }
+ end
+
+ def test_annotate_binary_file
+ get :annotate, :id => 3, :path => ['images', 'edit.png']
+ assert_response 500
+ assert_tag :tag => 'div', :attributes => { :class => /error/ },
+ :content => /can not be annotated/
+ end
+ else
+ puts "Git test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesMercurialControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_show_root
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal 3, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+ end
+
+ def test_show_directory
+ get :show, :id => 3, :path => ['images']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_show_at_given_revision
+ get :show, :id => 3, :path => ['images'], :rev => 0
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => 3, :path => ['images', 'edit.png']
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'edit.png'
+ end
+
+ def test_entry_show
+ get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => /10/,
+ :attributes => { :class => /line-num/ },
+ :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
+ end
+
+ def test_entry_download
+ get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('WITHOUT ANY WARRANTY')
+ end
+
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['sources']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
+ def test_diff
+ # Full diff of changeset 4
+ get :diff, :id => 3, :rev => 4
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /22/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_out/ },
+ :content => /def remove/ }
+ end
+
+ def test_annotate
+ get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
+ assert_response :success
+ assert_template 'annotate'
+ # Line 23, revision 4
+ assert_tag :tag => 'th', :content => /23/,
+ :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /4/ } },
+ :sibling => { :tag => 'td', :content => /jsmith/ },
+ :sibling => { :tag => 'td', :content => /watcher =/ }
+ end
+ else
+ puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesSubversionControllerTest < ActionController::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
+ :repositories, :issues, :issue_statuses, :changesets, :changes,
+ :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
+
+ # No '..' in the repository path for svn
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ Setting.default_language = 'en'
+ User.current = nil
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :show, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
+ assert_equal 'dir', entry.kind
+ end
+
+ def test_browse_directory
+ get :show, :id => 1, :path => ['subversion_test']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
+ assert_equal 'file', entry.kind
+ assert_equal 'subversion_test/helloworld.c', entry.path
+ assert_tag :a, :content => 'helloworld.c', :attributes => { :class => /text\-x\-c/ }
+ end
+
+ def test_browse_at_given_revision
+ get :show, :id => 1, :path => ['subversion_test'], :rev => 4
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
+ end
+
+ def test_file_changes
+ get :changes, :id => 1, :path => ['subversion_test', 'folder', 'helloworld.rb' ]
+ assert_response :success
+ assert_template 'changes'
+
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert_equal %w(6 3 2), changesets.collect(&:revision)
+
+ # svn properties displayed with svn >= 1.5 only
+ if Redmine::Scm::Adapters::SubversionAdapter.client_version_above?([1, 5, 0])
+ assert_not_nil assigns(:properties)
+ assert_equal 'native', assigns(:properties)['svn:eol-style']
+ assert_tag :ul,
+ :child => { :tag => 'li',
+ :child => { :tag => 'b', :content => 'svn:eol-style' },
+ :child => { :tag => 'span', :content => 'native' } }
+ end
+ end
+
+ def test_directory_changes
+ get :changes, :id => 1, :path => ['subversion_test', 'folder' ]
+ assert_response :success
+ assert_template 'changes'
+
+ changesets = assigns(:changesets)
+ assert_not_nil changesets
+ assert_equal %w(10 9 7 6 5 2), changesets.collect(&:revision)
+ end
+
+ def test_entry
+ get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
+ assert_response :success
+ assert_template 'entry'
+ end
+
+ def test_entry_should_send_if_too_big
+ # no files in the test repo is larger than 1KB...
+ with_settings :file_max_size_displayed => 0 do
+ get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
+ assert_response :success
+ assert_template ''
+ assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition']
+ end
+ end
+
+ def test_entry_at_given_revision
+ get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3 and file was moved in r6
+ assert_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /Here's the code/
+ end
+
+ def test_entry_not_found
+ get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
+ assert_tag :tag => 'div', :attributes => { :class => /error/ },
+ :content => /The entry or revision was not found in the repository/
+ end
+
+ def test_entry_download
+ get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw'
+ assert_response :success
+ assert_template ''
+ assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition']
+ end
+
+ def test_directory_entry
+ get :entry, :id => 1, :path => ['subversion_test', 'folder']
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entry)
+ assert_equal 'folder', assigns(:entry).name
+ end
+
+ def test_revision
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+ assert_tag :tag => 'ul',
+ :child => { :tag => 'li',
+ # link to the entry at rev 2
+ :child => { :tag => 'a',
+ :attributes => {:href => '/projects/ecookbook/repository/revisions/2/entry/test/some/path/in/the/repo'},
+ :content => 'repo',
+ # link to partial diff
+ :sibling => { :tag => 'a',
+ :attributes => { :href => '/projects/ecookbook/repository/revisions/2/diff/test/some/path/in/the/repo' }
+ }
+ }
+ }
+ end
+
+ def test_revision_with_repository_pointing_to_a_subdirectory
+ r = Project.find(1).repository
+ # Changes repository url to a subdirectory
+ r.update_attribute :url, (r.url + '/test/some')
+
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+ assert_tag :tag => 'ul',
+ :child => { :tag => 'li',
+ # link to the entry at rev 2
+ :child => { :tag => 'a',
+ :attributes => {:href => '/projects/ecookbook/repository/revisions/2/entry/path/in/the/repo'},
+ :content => 'repo',
+ # link to partial diff
+ :sibling => { :tag => 'a',
+ :attributes => { :href => '/projects/ecookbook/repository/revisions/2/diff/path/in/the/repo' }
+ }
+ }
+ }
+ end
+
+ def test_revision_diff
+ get :diff, :id => 1, :rev => 3
+ assert_response :success
+ assert_template 'diff'
+ end
+
+ def test_directory_diff
+ get :diff, :id => 1, :rev => 6, :rev_to => 2, :path => ['subversion_test', 'folder']
+ assert_response :success
+ assert_template 'diff'
+
+ diff = assigns(:diff)
+ assert_not_nil diff
+ # 2 files modified
+ assert_equal 2, Redmine::UnifiedDiff.new(diff).size
+ end
+
+ def test_annotate
+ get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c']
+ assert_response :success
+ assert_template 'annotate'
+ end
+ else
+ puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'roles_controller'
+
+# Re-raise errors caught by the controller.
+class RolesController; def rescue_action(e) raise e end; end
+
+class RolesControllerTest < ActionController::TestCase
+ fixtures :roles, :users, :members, :member_roles, :workflows
+
+ def setup
+ @controller = RolesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_get_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:roles)
+ assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
+
+ assert_tag :tag => 'a', :attributes => { :href => '/roles/edit/1' },
+ :content => 'Manager'
+ end
+
+ def test_get_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_new_with_validaton_failure
+ post :new, :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_post_new_without_workflow_copy
+ post :new, :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_post_new_with_workflow_copy
+ post :new, :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).workflows.size, role.workflows.size
+ end
+
+ def test_get_edit
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_equal Role.find(1), assigns(:role)
+ end
+
+ def test_post_edit
+ post :edit, :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_destroy
+ r = Role.new(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
+ assert r.save
+
+ post :destroy, :id => r
+ assert_redirected_to 'roles'
+ assert_nil Role.find_by_id(r.id)
+ end
+
+ def test_destroy_role_in_use
+ post :destroy, :id => 1
+ assert_redirected_to 'roles'
+ assert flash[:error] == 'This role is in use and can not be deleted.'
+ assert_not_nil Role.find_by_id(1)
+ end
+
+ def test_get_report
+ get :report
+ assert_response :success
+ assert_template 'report'
+
+ assert_not_nil assigns(:roles)
+ assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
+
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'permissions[3][]',
+ :value => 'add_issues',
+ :checked => 'checked' }
+
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'permissions[3][]',
+ :value => 'delete_issues',
+ :checked => nil }
+ end
+
+ def test_post_report
+ post :report, :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 :report, :permissions => { '0' => '' }
+ assert_redirected_to 'roles'
+ assert Role.find(1).permissions.empty?
+ end
+
+ def test_move_highest
+ post :edit, :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
+ post :edit, :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
+ post :edit, :id => 2, :role => {:move_to => 'lower'}
+ assert_redirected_to 'roles'
+ assert_equal position + 1, Role.find(2).position
+ end
+
+ def test_move_lowest
+ post :edit, :id => 2, :role => {:move_to => 'lowest'}
+ assert_redirected_to 'roles'
+ assert_equal Role.count, Role.find(2).position
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+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,
+ :issues, :trackers, :issue_statuses,
+ :custom_fields, :custom_values,
+ :repositories, :changesets
+
+ def setup
+ @controller = SearchController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_search_for_projects
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ get :index, :q => "cook"
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Project.find(1))
+ end
+
+ def test_search_all_projects
+ get :index, :q => 'recipe subproject commit', :submit => 'Search'
+ assert_response :success
+ assert_template 'index'
+
+ assert assigns(:results).include?(Issue.find(2))
+ assert assigns(:results).include?(Issue.find(5))
+ assert assigns(:results).include?(Changeset.find(101))
+ assert_tag :dt, :attributes => { :class => /issue/ },
+ :child => { :tag => 'a', :content => /Add ingredients categories/ },
+ :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
+
+ assert assigns(:results_by_type).is_a?(Hash)
+ assert_equal 5, assigns(:results_by_type)['changesets']
+ assert_tag :a, :content => 'Changesets (5)'
+ end
+
+ def test_search_issues
+ get :index, :q => 'issue', :issues => 1
+ assert_response :success
+ assert_template 'index'
+
+ assert assigns(:results).include?(Issue.find(8))
+ assert assigns(:results).include?(Issue.find(5))
+ assert_tag :dt, :attributes => { :class => /issue closed/ },
+ :child => { :tag => 'a', :content => /Closed/ }
+ end
+
+ def test_search_project_and_subprojects
+ get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :submit => 'Search'
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Issue.find(1))
+ assert assigns(:results).include?(Issue.find(5))
+ end
+
+ def test_search_without_searchable_custom_fields
+ CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
+
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:project)
+
+ get :index, :id => 1, :q => "can"
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_search_with_searchable_custom_fields
+ get :index, :id => 1, :q => "stringforcustomfield"
+ assert_response :success
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 1, results.size
+ assert results.include?(Issue.find(3))
+ end
+
+ def test_search_all_words
+ # 'all words' is on by default
+ get :index, :id => 1, :q => 'recipe updating saving'
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 1, results.size
+ assert results.include?(Issue.find(3))
+ end
+
+ def test_search_one_of_the_words
+ get :index, :id => 1, :q => 'recipe updating saving', :submit => 'Search'
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 3, results.size
+ assert results.include?(Issue.find(3))
+ end
+
+ def test_search_titles_only_without_result
+ get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1', :titles_only => '1', :submit => 'Search'
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 0, results.size
+ end
+
+ def test_search_titles_only
+ get :index, :id => 1, :q => 'recipe', :titles_only => '1', :submit => 'Search'
+ results = assigns(:results)
+ assert_not_nil results
+ assert_equal 2, results.size
+ end
+
+ def test_search_with_invalid_project_id
+ get :index, :id => 195, :q => 'recipe'
+ assert_response 404
+ assert_nil assigns(:results)
+ end
+
+ def test_quick_jump_to_issue
+ # issue of a public project
+ get :index, :q => "3"
+ assert_redirected_to 'issues/3'
+
+ # issue of a private project
+ get :index, :q => "4"
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_tokens_with_quotes
+ get :index, :id => 1, :q => '"good bye" hello "bye bye"'
+ assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_get_edit
+ get :edit
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_post_edit_notifications
+ post :edit, :settings => {:mail_from => 'functional@test.foo',
+ :bcc_recipients => '0',
+ :notified_events => %w(issue_added issue_updated news_added),
+ :emails_footer => 'Test footer'
+ }
+ assert_redirected_to 'settings/edit'
+ assert_equal 'functional@test.foo', Setting.mail_from
+ assert !Setting.bcc_recipients?
+ assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
+ assert_equal 'Test footer', Setting.emails_footer
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'sys_controller'
+
+# Re-raise errors caught by the controller.
+class SysController; def rescue_action(e) raise e end; end
+
+class SysControllerTest < ActionController::TestCase
+ fixtures :projects, :repositories
+
+ 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
+
+ def test_projects_with_repository_enabled
+ get :projects
+ assert_response :success
+ assert_equal 'application/xml', @response.content_type
+ with_options :tag => 'projects' do |test|
+ test.assert_tag :children => { :count => Project.active.has_module(:repository).count }
+ end
+ end
+
+ def test_create_project_repository
+ assert_nil Project.find(4).repository
+
+ post :create_project_repository, :id => 4,
+ :vendor => 'Subversion',
+ :repository => { :url => 'file:///create/project/repository/subproject2'}
+ assert_response :created
+
+ r = Project.find(4).repository
+ assert r.is_a?(Repository::Subversion)
+ assert_equal 'file:///create/project/repository/subproject2', r.url
+ end
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = TimelogController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/issues/567/time_entries/new'},
+ :controller => 'timelog', :action => 'edit', :issue_id => '567'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/ecookbook/time_entries/new'},
+ :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/ecookbook/issues/567/time_entries/new'},
+ :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook', :issue_id => '567'
+ )
+
+ #TODO: change new form to POST to issue_time_entries_path instead of to edit action
+ #TODO: change edit form to PUT to time_entry_path
+ assert_routing(
+ {:method => :get, :path => '/time_entries/22/edit'},
+ :controller => 'timelog', :action => 'edit', :id => '22'
+ )
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 3
+ get :edit, :project_id => 1
+ assert_response :success
+ assert_template 'edit'
+ # Default activity selected
+ assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
+ :content => 'Development'
+ end
+
+ def test_get_edit_existing_time
+ @request.session[:user_id] = 2
+ get :edit, :id => 2, :project_id => nil
+ assert_response :success
+ assert_template 'edit'
+ # Default activity selected
+ assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/timelog/edit/2' }
+ end
+
+ def test_get_edit_should_only_show_active_time_entry_activities
+ @request.session[:user_id] = 3
+ get :edit, :project_id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'option', :content => 'Inactive Activity'
+
+ end
+
+ def test_get_edit_with_an_existing_time_entry_with_inactive_activity
+ te = TimeEntry.find(1)
+ te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
+ te.save!
+
+ @request.session[:user_id] = 1
+ get :edit, :project_id => 1, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ # Blank option since nothing is pre-selected
+ assert_tag :tag => 'option', :content => '--- Please select ---'
+ end
+
+ def test_post_edit
+ # TODO: should POST to issues’ time log instead of project. change form
+ # and routing
+ @request.session[:user_id] = 3
+ post :edit, :project_id => 1,
+ :time_entry => {:comments => 'Some work on TimelogControllerTest',
+ # Not the default activity
+ :activity_id => '11',
+ :spent_on => '2008-03-14',
+ :issue_id => '1',
+ :hours => '7.3'}
+ assert_redirected_to :action => 'details', :project_id => 'ecookbook'
+
+ i = Issue.find(1)
+ t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
+ assert_not_nil t
+ assert_equal 11, t.activity_id
+ assert_equal 7.3, t.hours
+ assert_equal 3, t.user_id
+ assert_equal i, t.issue
+ assert_equal i.project, t.project
+ end
+
+ def test_update
+ entry = TimeEntry.find(1)
+ assert_equal 1, entry.issue_id
+ assert_equal 2, entry.user_id
+
+ @request.session[:user_id] = 1
+ post :edit, :id => 1,
+ :time_entry => {:issue_id => '2',
+ :hours => '8'}
+ assert_redirected_to :action => 'details', :project_id => 'ecookbook'
+ entry.reload
+
+ assert_equal 8, entry.hours
+ assert_equal 2, entry.issue_id
+ assert_equal 2, entry.user_id
+ end
+
+ def test_destroy_routing
+ #TODO: use DELETE to time_entry_path
+ assert_routing(
+ {:method => :post, :path => '/time_entries/55/destroy'},
+ :controller => 'timelog', :action => 'destroy', :id => '55'
+ )
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1
+ assert_redirected_to :action => 'details', :project_id => 'ecookbook'
+ assert_nil TimeEntry.find_by_id(1)
+ end
+
+ def test_report_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/time_entries/report'},
+ :controller => 'timelog', :action => 'report', :project_id => '567'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/time_entries/report.csv'},
+ :controller => 'timelog', :action => 'report', :project_id => '567', :format => 'csv'
+ )
+ end
+
+ def test_report_no_criteria
+ get :report, :project_id => 1
+ assert_response :success
+ assert_template 'report'
+ end
+
+ def test_report_routing_for_all_projects
+ assert_routing(
+ {:method => :get, :path => '/time_entries/report'},
+ :controller => 'timelog', :action => 'report'
+ )
+ end
+
+ def test_report_all_projects
+ get :report
+ assert_response :success
+ assert_template 'report'
+ end
+
+ def test_report_all_projects_denied
+ r = Role.anonymous
+ r.permissions.delete(:view_time_entries)
+ r.permissions_will_change!
+ r.save
+ get :report
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
+ end
+
+ def test_report_all_projects_one_criteria
+ get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "8.65", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_all_time
+ get :report, :project_id => 1, :criterias => ['project', 'issue']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_all_time_by_day
+ get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ assert_tag :tag => 'th', :content => '2007-03-12'
+ end
+
+ def test_report_one_criteria
+ get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "8.65", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_two_criterias
+ get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_one_day
+ get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "4.25", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_at_issue_level
+ get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "154.25", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_custom_field_criteria
+ get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_not_nil assigns(:criterias)
+ assert_equal 3, assigns(:criterias).size
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ # Custom field column
+ assert_tag :tag => 'th', :content => 'Database'
+ # 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'
+ end
+
+ def test_report_one_criteria_no_result
+ get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "0.00", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_all_projects_csv_export
+ get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv', @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
+ # Total row
+ assert_equal 'Total,"","","","",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", :criterias => ["project", "member", "activity"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv', @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
+ # Total row
+ assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
+ end
+
+ def test_details_all_projects
+ get :details
+ assert_response :success
+ assert_template 'details'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_project_details_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/time_entries'},
+ :controller => 'timelog', :action => 'details', :project_id => '567'
+ )
+ end
+
+ def test_details_at_project_level
+ get :details, :project_id => 1
+ assert_response :success
+ assert_template 'details'
+ assert_not_nil assigns(:entries)
+ assert_equal 4, assigns(:entries).size
+ # project and subproject
+ 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_equal '2007-03-11'.to_date, assigns(:from)
+ assert_equal '2007-04-22'.to_date, assigns(:to)
+ end
+
+ def test_details_at_project_level_with_date_range
+ get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
+ assert_response :success
+ assert_template 'details'
+ 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_equal '2007-03-20'.to_date, assigns(:from)
+ assert_equal '2007-04-30'.to_date, assigns(:to)
+ end
+
+ def test_details_at_project_level_with_period
+ get :details, :project_id => 1, :period => '7_days'
+ assert_response :success
+ assert_template 'details'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:total_hours)
+ assert_equal Date.today - 7, assigns(:from)
+ assert_equal Date.today, assigns(:to)
+ end
+
+ def test_details_one_day
+ get :details, :project_id => 1, :from => "2007-03-23", :to => "2007-03-23"
+ assert_response :success
+ assert_template 'details'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "4.25", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_issue_details_routing
+ assert_routing(
+ {:method => :get, :path => 'time_entries'},
+ :controller => 'timelog', :action => 'details'
+ )
+ assert_routing(
+ {:method => :get, :path => '/issues/234/time_entries'},
+ :controller => 'timelog', :action => 'details', :issue_id => '234'
+ )
+ # TODO: issue detail page shouldnt link to project_issue_time_entries_path but to normal issues one
+ # doesnt seem to have effect on resulting page so controller can be left untouched
+ assert_routing(
+ {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries'},
+ :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123'
+ )
+ end
+
+ def test_details_at_issue_level
+ get :details, :issue_id => 1
+ assert_response :success
+ assert_template 'details'
+ assert_not_nil assigns(:entries)
+ assert_equal 2, assigns(:entries).size
+ assert_not_nil assigns(:total_hours)
+ assert_equal 154.25, assigns(:total_hours)
+ # display all time by default
+ assert_equal '2007-03-11'.to_date, assigns(:from)
+ assert_equal '2007-04-22'.to_date, assigns(:to)
+ end
+
+ def test_details_formatted_routing
+ assert_routing(
+ {:method => :get, :path => 'time_entries.atom'},
+ :controller => 'timelog', :action => 'details', :format => 'atom'
+ )
+ assert_routing(
+ {:method => :get, :path => 'time_entries.csv'},
+ :controller => 'timelog', :action => 'details', :format => 'csv'
+ )
+ end
+
+ def test_details_for_project_formatted_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/time_entries.atom'},
+ :controller => 'timelog', :action => 'details', :format => 'atom', :project_id => '567'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/time_entries.csv'},
+ :controller => 'timelog', :action => 'details', :format => 'csv', :project_id => '567'
+ )
+ end
+
+ def test_details_for_issue_formatted_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries.atom'},
+ :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123', :format => 'atom'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries.csv'},
+ :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123', :format => 'csv'
+ )
+ end
+
+ def test_details_atom_feed
+ get :details, :project_id => 1, :format => 'atom'
+ assert_response :success
+ assert_equal 'application/atom+xml', @response.content_type
+ assert_not_nil assigns(:items)
+ assert assigns(:items).first.is_a?(TimeEntry)
+ end
+
+ def test_details_all_projects_csv_export
+ Setting.date_format = '%m/%d/%Y'
+ get :details, :format => 'csv'
+ assert_response :success
+ assert_equal 'text/csv', @response.content_type
+ assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\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_details_csv_export
+ Setting.date_format = '%m/%d/%Y'
+ get :details, :project_id => 1, :format => 'csv'
+ assert_response :success
+ assert_equal 'text/csv', @response.content_type
+ assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
+ assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = TrackersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_get_new
+ get :new
+ assert_response :success
+ assert_template 'new'
+ end
+
+ def test_post_new
+ post :new, :tracker => { :name => 'New tracker', :project_ids => ['1', '', ''] }
+ assert_redirected_to '/trackers/list'
+ tracker = Tracker.find_by_name('New tracker')
+ assert_equal [1], tracker.project_ids.sort
+ assert_equal 0, tracker.workflows.count
+ end
+
+ def test_post_new_with_workflow_copy
+ post :new, :tracker => { :name => 'New tracker' }, :copy_workflow_from => 1
+ assert_redirected_to '/trackers/list'
+ tracker = Tracker.find_by_name('New tracker')
+ assert_equal 0, tracker.projects.count
+ assert_equal Tracker.find(1).workflows.count, tracker.workflows.count
+ end
+
+ def test_get_edit
+ Tracker.find(1).project_ids = [1, 3]
+
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '1',
+ :checked => 'checked' }
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '2',
+ :checked => nil }
+
+ assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
+ :value => '',
+ :type => 'hidden'}
+ end
+
+ def test_post_edit
+ post :edit, :id => 1, :tracker => { :name => 'Renamed',
+ :project_ids => ['1', '2', ''] }
+ assert_redirected_to '/trackers/list'
+ assert_equal [1, 2], Tracker.find(1).project_ids.sort
+ end
+
+ def test_post_edit_without_projects
+ post :edit, :id => 1, :tracker => { :name => 'Renamed',
+ :project_ids => [''] }
+ assert_redirected_to '/trackers/list'
+ assert Tracker.find(1).project_ids.empty?
+ end
+
+ def test_move_lower
+ tracker = Tracker.find_by_position(1)
+ post :edit, :id => 1, :tracker => { :move_to => 'lower' }
+ assert_equal 2, tracker.reload.position
+ end
+
+ def test_destroy
+ tracker = Tracker.create!(:name => 'Destroyable')
+ assert_difference 'Tracker.count', -1 do
+ post :destroy, :id => tracker.id
+ end
+ assert_redirected_to '/trackers/list'
+ assert_nil flash[:error]
+ end
+
+ def test_destroy_tracker_in_use
+ assert_no_difference 'Tracker.count' do
+ post :destroy, :id => 1
+ end
+ assert_redirected_to '/trackers/list'
+ assert_not_nil flash[:error]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ fixtures :users, :projects, :members, :member_roles, :roles
+
+ def setup
+ @controller = UsersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index_routing
+ assert_generates(
+ '/users',
+ :controller => 'users', :action => 'index'
+ )
+ assert_routing(
+ {:method => :get, :path => '/users'},
+ :controller => 'users', :action => 'index'
+ )
+ assert_recognizes(
+ {:controller => 'users', :action => 'index'},
+ {:method => :get, :path => '/users'}
+ )
+ end
+
+ def test_index
+ 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?}
+ end
+
+ def test_index_with_name_filter
+ get :index, :name => 'john'
+ assert_response :success
+ assert_template 'index'
+ users = assigns(:users)
+ assert_not_nil users
+ assert_equal 1, users.size
+ assert_equal 'John', users.first.firstname
+ end
+
+ def test_show_routing
+ assert_routing(
+ {:method => :get, :path => '/users/44'},
+ :controller => 'users', :action => 'show', :id => '44'
+ )
+ assert_recognizes(
+ {:controller => 'users', :action => 'show', :id => '44'},
+ {:method => :get, :path => '/users/44'}
+ )
+ end
+
+ def test_show
+ @request.session[:user_id] = nil
+ get :show, :id => 2
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:user)
+ end
+
+ def test_show_should_not_fail_when_custom_values_are_nil
+ user = User.find(2)
+
+ # Create a custom field to illustrate the issue
+ custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text')
+ custom_value = user.custom_values.build(:custom_field => custom_field).save!
+
+ get :show, :id => 2
+ assert_response :success
+ end
+
+
+ def test_show_inactive
+ get :show, :id => 5
+ assert_response 404
+ assert_nil assigns(:user)
+ end
+
+ def test_show_should_not_reveal_users_with_no_visible_activity_or_project
+ @request.session[:user_id] = nil
+ get :show, :id => 9
+ assert_response 404
+ end
+
+ def test_add_routing
+ assert_routing(
+ {:method => :get, :path => '/users/new'},
+ :controller => 'users', :action => 'add'
+ )
+ assert_recognizes(
+ #TODO: remove this and replace with POST to collection, need to modify form
+ {:controller => 'users', :action => 'add'},
+ {:method => :post, :path => '/users/new'}
+ )
+ assert_recognizes(
+ {:controller => 'users', :action => 'add'},
+ {:method => :post, :path => '/users'}
+ )
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/users/444/edit'},
+ :controller => 'users', :action => 'edit', :id => '444'
+ )
+ assert_routing(
+ {:method => :get, :path => '/users/222/edit/membership'},
+ :controller => 'users', :action => 'edit', :id => '222', :tab => 'membership'
+ )
+ assert_recognizes(
+ #TODO: use PUT on user_path, modify form
+ {:controller => 'users', :action => 'edit', :id => '444'},
+ {:method => :post, :path => '/users/444/edit'}
+ )
+ end
+
+ def test_edit
+ ActionMailer::Base.deliveries.clear
+ post :edit, :id => 2, :user => {:firstname => 'Changed'}
+ assert_equal 'Changed', User.find(2).firstname
+ assert ActionMailer::Base.deliveries.empty?
+ end
+
+ def test_edit_with_activation_should_send_a_notification
+ u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr')
+ u.login = 'foo'
+ u.status = User::STATUS_REGISTERED
+ u.save!
+ ActionMailer::Base.deliveries.clear
+ Setting.bcc_recipients = '1'
+
+ post :edit, :id => u.id, :user => {:status => User::STATUS_ACTIVE}
+ assert u.reload.active?
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal ['foo.bar@somenet.foo'], mail.bcc
+ assert mail.body.include?(ll('fr', :notice_account_activated))
+ end
+
+ def test_edit_with_password_change_should_send_a_notification
+ ActionMailer::Base.deliveries.clear
+ Setting.bcc_recipients = '1'
+
+ u = User.find(2)
+ post :edit, :id => u.id, :user => {}, :password => 'newpass', :password_confirmation => 'newpass', :send_information => '1'
+ assert_equal User.hash_password('newpass'), u.reload.hashed_password
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal [u.mail], mail.bcc
+ assert mail.body.include?('newpass')
+ end
+
+ def test_add_membership_routing
+ assert_routing(
+ {:method => :post, :path => '/users/123/memberships'},
+ :controller => 'users', :action => 'edit_membership', :id => '123'
+ )
+ end
+
+ def test_edit_membership_routing
+ assert_routing(
+ {:method => :post, :path => '/users/123/memberships/55'},
+ :controller => 'users', :action => 'edit_membership', :id => '123', :membership_id => '55'
+ )
+ end
+
+ def test_edit_membership
+ post :edit_membership, :id => 2, :membership_id => 1,
+ :membership => { :role_ids => [2]}
+ assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships'
+ assert_equal [2], Member.find(1).role_ids
+ end
+
+ def test_destroy_membership
+ assert_routing(
+ #TODO: use DELETE method on user_membership_path, modify form
+ {:method => :post, :path => '/users/567/memberships/12/destroy'},
+ :controller => 'users', :action => 'destroy_membership', :id => '567', :membership_id => '12'
+ )
+ end
+
+ def test_destroy_membership
+ post :destroy_membership, :id => 2, :membership_id => 1
+ assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships'
+ assert_nil Member.find_by_id(1)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = VersionsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_show
+ get :show, :id => 2
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:version)
+
+ assert_tag :tag => 'h2', :content => /1.0/
+ end
+
+ def test_get_edit
+ @request.session[:user_id] = 2
+ get :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_close_completed
+ Version.update_all("status = 'open'")
+ @request.session[:user_id] = 2
+ post :close_completed, :project_id => 'ecookbook'
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ assert_not_nil Version.find_by_status('closed')
+ end
+
+ def test_post_edit
+ @request.session[:user_id] = 2
+ post :edit, :id => 2,
+ :version => { :name => 'New version name',
+ :effective_date => Date.today.strftime("%Y-%m-%d")}
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ version = Version.find(2)
+ assert_equal 'New version name', version.name
+ assert_equal Date.today, version.effective_date
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 3
+ assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
+ assert_nil Version.find_by_id(3)
+ end
+
+ def test_issue_status_by
+ xhr :get, :status_by, :id => 2
+ assert_response :success
+ assert_template '_issue_counts'
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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_get_watch_should_be_invalid
+ @request.session[:user_id] = 3
+ get :watch, :object_type => 'issue', :object_id => '1'
+ assert_response 405
+ end
+
+ def test_watch
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => '1'
+ assert_response :success
+ assert_select_rjs :replace_html, 'watcher'
+ end
+ assert Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_unwatch
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count', -1) do
+ xhr :post, :unwatch, :object_type => 'issue', :object_id => '2'
+ assert_response :success
+ assert_select_rjs :replace_html, 'watcher'
+ end
+ assert !Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_new_watcher
+ @request.session[:user_id] = 2
+ assert_difference('Watcher.count') do
+ xhr :post, :new, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
+ assert_response :success
+ assert_select_rjs :replace_html, 'watchers'
+ end
+ assert Issue.find(2).watched_by?(User.find(4))
+ end
+
+ 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'
+ assert_response :success
+ assert_select_rjs :replace_html, 'watchers'
+ end
+ assert !Issue.find(2).watched_by?(User.find(3))
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = WelcomeController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+ assert_not_nil assigns(:news)
+ assert_not_nil assigns(:projects)
+ assert !assigns(:projects).include?(Project.find(:first, :conditions => {:is_public => false}))
+ end
+
+ def test_browser_language
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+ get :index
+ assert_equal :fr, @controller.current_language
+ end
+
+ def test_browser_language_alternate
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW'
+ get :index
+ assert_equal :"zh-TW", @controller.current_language
+ end
+
+ def test_browser_language_alternate_not_valid
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA'
+ get :index
+ assert_equal :fr, @controller.current_language
+ end
+
+ def test_robots
+ get :robots
+ assert_response :success
+ assert_equal 'text/plain', @response.content_type
+ assert @response.body.match(%r{^Disallow: /projects/ecookbook/issues$})
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
+
+ def setup
+ @controller = WikiController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_index_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki'},
+ :controller => 'wiki', :action => 'index', :id => '567'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/lalala'},
+ :controller => 'wiki', :action => 'index', :id => '567', :page => 'lalala'
+ )
+ assert_generates(
+ '/projects/567/wiki',
+ :controller => 'wiki', :action => 'index', :id => '567', :page => nil
+ )
+ end
+
+ def test_show_start_page
+ get :index, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'h1', :content => /CookBook documentation/
+
+ # child_pages macro
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } }
+ end
+
+ def test_show_page_with_name
+ get :index, :id => 1, :page => 'Another_page'
+ assert_response :success
+ assert_template 'show'
+ 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',
+ :alt => 'This is a logo' }
+ end
+
+ def test_show_unexistent_page_without_edit_right
+ get :index, :id => 1, :page => 'Unexistent page'
+ assert_response 404
+ end
+
+ def test_show_unexistent_page_with_edit_right
+ @request.session[:user_id] = 2
+ get :index, :id => 1, :page => 'Unexistent page'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_edit_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/my_page/edit'},
+ :controller => 'wiki', :action => 'edit', :id => '567', :page => 'my_page'
+ )
+ assert_recognizes(#TODO: use PUT to page path, adjust forms accordingly
+ {:controller => 'wiki', :action => 'edit', :id => '567', :page => 'my_page'},
+ {:method => :post, :path => '/projects/567/wiki/my_page/edit'}
+ )
+ end
+
+ def test_create_page
+ @request.session[:user_id] = 2
+ post :edit, :id => 1,
+ :page => 'New page',
+ :content => {:comments => 'Created the page',
+ :text => "h1. New page\n\nThis is a new page",
+ :version => 0}
+ assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'New_page'
+ page = Project.find(1).wiki.find_page('New page')
+ assert !page.new_record?
+ assert_not_nil page.content
+ assert_equal 'Created the page', page.content.comments
+ end
+
+ def test_preview_routing
+ assert_routing(
+ {:method => :post, :path => '/projects/567/wiki/CookBook_documentation/preview'},
+ :controller => 'wiki', :action => 'preview', :id => '567', :page => 'CookBook_documentation'
+ )
+ end
+
+ def test_preview
+ @request.session[:user_id] = 2
+ xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
+ :content => { :comments => '',
+ :text => 'this is a *previewed text*',
+ :version => 3 }
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'strong', :content => /previewed text/
+ end
+
+ def test_preview_new_page
+ @request.session[:user_id] = 2
+ xhr :post, :preview, :id => 1, :page => 'New page',
+ :content => { :text => 'h1. New page',
+ :comments => '',
+ :version => 0 }
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'h1', :content => /New page/
+ end
+
+ def test_history_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/history'},
+ :controller => 'wiki', :action => 'history', :id => '1', :page => 'CookBook_documentation'
+ )
+ end
+
+ def test_history
+ get :history, :id => 1, :page => 'CookBook_documentation'
+ assert_response :success
+ assert_template 'history'
+ assert_not_nil assigns(:versions)
+ assert_equal 3, assigns(:versions).size
+ assert_select "input[type=submit][name=commit]"
+ end
+
+ def test_history_with_one_version
+ get :history, :id => 1, :page => 'Another_page'
+ assert_response :success
+ assert_template 'history'
+ assert_not_nil assigns(:versions)
+ assert_equal 1, assigns(:versions).size
+ assert_select "input[type=submit][name=commit]", false
+ end
+
+ def test_diff_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/diff/2/vs/1'},
+ :controller => 'wiki', :action => 'diff', :id => '1', :page => 'CookBook_documentation', :version => '2', :version_from => '1'
+ )
+ end
+
+ def test_diff
+ get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
+ assert_response :success
+ assert_template 'diff'
+ assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
+ :content => /updated/
+ end
+
+ def test_annotate_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/annotate/2'},
+ :controller => 'wiki', :action => 'annotate', :id => '1', :page => 'CookBook_documentation', :version => '2'
+ )
+ end
+
+ def test_annotate
+ get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
+ assert_response :success
+ assert_template 'annotate'
+ # Line 1
+ assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
+ :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
+ :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
+ # Line 2
+ assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
+ :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
+ :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
+ end
+
+ def test_rename_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/22/wiki/ladida/rename'},
+ :controller => 'wiki', :action => 'rename', :id => '22', :page => 'ladida'
+ )
+ assert_recognizes(
+ #TODO: should be moved into a update action and use a PUT to the page URI
+ {:controller => 'wiki', :action => 'rename', :id => '22', :page => 'ladida'},
+ {:method => :post, :path => '/projects/22/wiki/ladida/rename'}
+ )
+ end
+
+ def test_rename_with_redirect
+ @request.session[:user_id] = 2
+ post :rename, :id => 1, :page => 'Another_page',
+ :wiki_page => { :title => 'Another renamed page',
+ :redirect_existing_links => 1 }
+ assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_renamed_page'
+ wiki = Project.find(1).wiki
+ # Check redirects
+ assert_not_nil wiki.find_page('Another page')
+ assert_nil wiki.find_page('Another page', :with_redirect => false)
+ end
+
+ def test_rename_without_redirect
+ @request.session[:user_id] = 2
+ post :rename, :id => 1, :page => 'Another_page',
+ :wiki_page => { :title => 'Another renamed page',
+ :redirect_existing_links => "0" }
+ assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_renamed_page'
+ wiki = Project.find(1).wiki
+ # Check that there's no redirects
+ assert_nil wiki.find_page('Another page')
+ end
+
+ def test_destroy_routing
+ assert_recognizes(
+ #TODO: should use DELETE on page URI
+ {:controller => 'wiki', :action => 'destroy', :id => '22', :page => 'ladida'},
+ {:method => :post, :path => 'projects/22/wiki/ladida/destroy'}
+ )
+ end
+
+ def test_destroy_child
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1, :page => 'Child_1'
+ assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
+ end
+
+ def test_destroy_parent
+ @request.session[:user_id] = 2
+ assert_no_difference('WikiPage.count') do
+ post :destroy, :id => 1, :page => 'Another_page'
+ end
+ assert_response :success
+ assert_template 'destroy'
+ end
+
+ def test_destroy_parent_with_nullify
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -1) do
+ post :destroy, :id => 1, :page => 'Another_page', :todo => 'nullify'
+ end
+ assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
+ assert_nil WikiPage.find_by_id(2)
+ end
+
+ def test_destroy_parent_with_cascade
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -3) do
+ post :destroy, :id => 1, :page => 'Another_page', :todo => 'destroy'
+ end
+ assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
+ assert_nil WikiPage.find_by_id(2)
+ assert_nil WikiPage.find_by_id(5)
+ end
+
+ def test_destroy_parent_with_reassign
+ @request.session[:user_id] = 2
+ assert_difference('WikiPage.count', -1) do
+ post :destroy, :id => 1, :page => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
+ end
+ assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
+ assert_nil WikiPage.find_by_id(2)
+ assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
+ end
+
+ def test_special_routing
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/page_index'},
+ :controller => 'wiki', :action => 'special', :id => '567', :page => 'page_index'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/Page_Index'},
+ :controller => 'wiki', :action => 'special', :id => '567', :page => 'Page_Index'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/date_index'},
+ :controller => 'wiki', :action => 'special', :id => '567', :page => 'date_index'
+ )
+ assert_routing(
+ {:method => :get, :path => '/projects/567/wiki/export'},
+ :controller => 'wiki', :action => 'special', :id => '567', :page => 'export'
+ )
+ end
+
+ def test_page_index
+ get :special, :id => 'ecookbook', :page => 'Page_index'
+ assert_response :success
+ assert_template 'special_page_index'
+ pages = assigns(:pages)
+ assert_not_nil pages
+ assert_equal Project.find(1).wiki.pages.size, pages.size
+
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
+ :content => 'CookBook documentation' },
+ :child => { :tag => 'ul',
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } } } },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
+ :content => 'Another page' } }
+ end
+
+ def test_not_found
+ get :index, :id => 999
+ assert_response 404
+ end
+
+ def test_protect_routing
+ assert_routing(
+ {:method => :post, :path => 'projects/22/wiki/ladida/protect'},
+ {:controller => 'wiki', :action => 'protect', :id => '22', :page => 'ladida'}
+ )
+ end
+
+ def test_protect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
+ assert !page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :id => 1, :page => page.title, :protected => '1'
+ assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_page'
+ assert page.reload.protected?
+ end
+
+ def test_unprotect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
+ assert page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :id => 1, :page => page.title, :protected => '0'
+ assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'CookBook_documentation'
+ assert !page.reload.protected?
+ end
+
+ def test_show_page_with_edit_link
+ @request.session[:user_id] = 2
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
+ end
+
+ def test_show_page_without_edit_link
+ @request.session[:user_id] = 4
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
+ end
+
+ def test_edit_unprotected_page
+ # Non members can edit unprotected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :id => 1, :page => 'Another_page'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_edit_protected_page_by_nonmember
+ # Non members can't edit protected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :id => 1, :page => 'CookBook_documentation'
+ assert_response 403
+ end
+
+ def test_edit_protected_page_by_member
+ @request.session[:user_id] = 2
+ get :edit, :id => 1, :page => 'CookBook_documentation'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_history_of_non_existing_page_should_return_404
+ get :history, :id => 1, :page => 'Unknown_page'
+ assert_response 404
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def test_edit_routing
+ assert_routing(
+ #TODO: use PUT
+ {:method => :post, :path => 'projects/ladida/wiki'},
+ :controller => 'wikis', :action => 'edit', :id => 'ladida'
+ )
+ end
+
+ def test_create
+ @request.session[:user_id] = 1
+ assert_nil Project.find(3).wiki
+ post :edit, :id => 3, :wiki => { :start_page => 'Start page' }
+ assert_response :success
+ wiki = Project.find(3).wiki
+ assert_not_nil wiki
+ assert_equal 'Start page', wiki.start_page
+ end
+
+ def test_destroy_routing
+ assert_routing(
+ {:method => :get, :path => 'projects/ladida/wiki/destroy'},
+ :controller => 'wikis', :action => 'destroy', :id => 'ladida'
+ )
+ assert_recognizes( #TODO: use DELETE and update form
+ {:controller => 'wikis', :action => 'destroy', :id => 'ladida'},
+ {:method => :post, :path => 'projects/ladida/wiki/destroy'}
+ )
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 1
+ post :destroy, :id => 1, :confirm => 1
+ assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'ecookbook', :tab => 'wiki'
+ assert_nil Project.find(1).wiki
+ end
+
+ def test_not_found
+ @request.session[:user_id] = 1
+ post :destroy, :id => 999, :confirm => 1
+ assert_response 404
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+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
+
+ def setup
+ @controller = WorkflowsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'index'
+
+ count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
+ assert_tag :tag => 'a', :content => count.to_s,
+ :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' }
+ end
+
+ def test_get_edit
+ get :edit
+ assert_response :success
+ assert_template 'edit'
+ assert_not_nil assigns(:roles)
+ assert_not_nil assigns(:trackers)
+ end
+
+ def test_get_edit_with_role_and_tracker
+ get :edit, :role_id => 2, :tracker_id => 1
+ assert_response :success
+ assert_template 'edit'
+ # allowed transitions
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[2][]',
+ :value => '1',
+ :checked => 'checked' }
+ # not allowed
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'issue_status[2][]',
+ :value => '3',
+ :checked => nil }
+ end
+
+ def test_post_edit
+ post :edit, :role_id => 2, :tracker_id => 1, :issue_status => {'4' => ['5'], '3' => ['1', '2']}
+ assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
+
+ assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
+ assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
+ assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
+ end
+
+ def test_clear_workflow
+ assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
+
+ post :edit, :role_id => 2, :tracker_id => 1
+ assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
+ end
+end
--- /dev/null
+# Re-raise errors caught by the controller.
+class StubController < ApplicationController
+ def rescue_action(e) raise e end;
+ attr_accessor :request, :url
+end
+
+class HelperTestCase < ActiveSupport::TestCase
+
+ # Add other helpers here if you need them
+ include ActionView::Helpers::ActiveRecordHelper
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::FormTagHelper
+ include ActionView::Helpers::FormOptionsHelper
+ include ActionView::Helpers::FormHelper
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::PrototypeHelper
+
+ def setup
+ super
+
+ @request = ActionController::TestRequest.new
+ @controller = StubController.new
+ @controller.request = @request
+
+ # Fake url rewriter so we can test url_for
+ @controller.url = ActionController::UrlRewriter.new @request, {}
+
+ ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
+ end
+
+ def test_dummy
+ # do nothing - required by test/unit
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__)}/../test_helper"
+
+begin
+ require 'mocha'
+rescue
+ # Won't run some tests
+end
+
+class AccountTest < ActionController::IntegrationTest
+ fixtures :users
+
+ # Replace this with your real tests.
+ def test_login
+ get "my/page"
+ assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
+ log_user('jsmith', 'jsmith')
+
+ get "my/account"
+ assert_response :success
+ assert_template "my/account"
+ end
+
+ def test_autologin
+ user = User.find(1)
+ Setting.autologin = "7"
+ Token.delete_all
+
+ # User logs in with 'autologin' checked
+ post '/login', :username => user.login, :password => 'admin', :autologin => 1
+ assert_redirected_to 'my/page'
+ token = Token.find :first
+ assert_not_nil token
+ assert_equal user, token.user
+ assert_equal 'autologin', token.action
+ assert_equal user.id, session[:user_id]
+ assert_equal token.value, cookies['autologin']
+
+ # Session is cleared
+ reset!
+ User.current = nil
+ # Clears user's last login timestamp
+ user.update_attribute :last_login_on, nil
+ assert_nil user.reload.last_login_on
+
+ # User comes back with his autologin cookie
+ cookies[:autologin] = token.value
+ get '/my/page'
+ assert_response :success
+ assert_template 'my/page'
+ assert_equal user.id, session[:user_id]
+ assert_not_nil user.reload.last_login_on
+ assert user.last_login_on.utc > 10.second.ago.utc
+ end
+
+ def test_lost_password
+ Token.delete_all
+
+ get "account/lost_password"
+ assert_response :success
+ assert_template "account/lost_password"
+
+ post "account/lost_password", :mail => 'jSmith@somenet.foo'
+ assert_redirected_to "/login"
+
+ token = Token.find(:first)
+ assert_equal 'recovery', token.action
+ assert_equal 'jsmith@somenet.foo', token.user.mail
+ assert !token.expired?
+
+ get "account/lost_password", :token => token.value
+ assert_response :success
+ assert_template "account/password_recovery"
+
+ post "account/lost_password", :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'newpass'
+ assert_redirected_to "/login"
+ assert_equal 'Password was successfully updated.', flash[:notice]
+
+ log_user('jsmith', 'newpass')
+ assert_equal 0, Token.count
+ end
+
+ def test_register_with_automatic_activation
+ Setting.self_registration = '3'
+
+ get 'account/register'
+ assert_response :success
+ assert_template 'account/register'
+
+ post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
+ :password => "newpass", :password_confirmation => "newpass"
+ assert_redirected_to 'my/account'
+ follow_redirect!
+ assert_response :success
+ assert_template 'my/account'
+
+ user = User.find_by_login('newuser')
+ assert_not_nil user
+ assert user.active?
+ assert_not_nil user.last_login_on
+ end
+
+ 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 => "newpass", :password_confirmation => "newpass"
+ assert_redirected_to '/login'
+ assert !User.find_by_login('newuser').active?
+ end
+
+ def test_register_with_email_activation
+ Setting.self_registration = '1'
+ Token.delete_all
+
+ post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
+ :password => "newpass", :password_confirmation => "newpass"
+ assert_redirected_to '/login'
+ assert !User.find_by_login('newuser').active?
+
+ token = Token.find(:first)
+ assert_equal 'register', token.action
+ assert_equal 'newuser@foo.bar', token.user.mail
+ assert !token.expired?
+
+ get 'account/activate', :token => token.value
+ assert_redirected_to '/login'
+ log_user('newuser', 'newpass')
+ end
+
+ if Object.const_defined?(:Mocha)
+
+ 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])
+
+ post 'account/login', :username => 'foo', :password => 'bar'
+ assert_redirected_to 'my/page'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+
+ 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])
+
+ post 'account/login', :username => 'foo', :password => 'bar'
+ assert_response :success
+ assert_template 'account/register'
+ assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
+ assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
+ 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'}
+ assert_redirected_to '/my/account'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+
+ def test_login_and_logout_should_clear_session
+ get '/login'
+ sid = session[:session_id]
+
+ post '/login', :username => 'admin', :password => 'admin'
+ assert_redirected_to 'my/page'
+ assert_not_equal sid, session[:session_id], "login should reset session"
+ assert_equal 1, session[:user_id]
+ sid = session[:session_id]
+
+ get '/'
+ assert_equal sid, session[:session_id]
+
+ get '/logout'
+ assert_not_equal sid, session[:session_id], "logout should reset session"
+ assert_nil session[:user_id]
+ end
+
+ else
+ puts 'Mocha is missing. Skipping tests.'
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require "#{File.dirname(__FILE__)}/../test_helper"
+
+class AdminTest < ActionController::IntegrationTest
+ fixtures :all
+
+ def test_add_user
+ log_user("admin", "admin")
+ get "/users/add"
+ assert_response :success
+ assert_template "users/add"
+ post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
+
+ user = User.find_by_login("psmith")
+ assert_kind_of User, user
+ assert_redirected_to "/users/#{ user.id }/edit"
+
+ logged_user = User.try_to_login("psmith", "psmith09")
+ assert_kind_of User, logged_user
+ assert_equal "Paul", logged_user.firstname
+
+ post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
+ assert_redirected_to "/users/#{ user.id }/edit"
+ locked_user = User.try_to_login("psmith", "psmith09")
+ assert_equal nil, locked_user
+ end
+
+ test "Add a user as an anonymous user should fail" do
+ post '/users/add', :user => { :login => 'psmith', :firstname => 'Paul'}, :password => "psmith09", :password_confirmation => "psmith09"
+ assert_response :redirect
+ assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers%2Fnew"
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__)}/../test_helper"
+
+class ApplicationTest < ActionController::IntegrationTest
+ include Redmine::I18n
+
+ fixtures :all
+
+ def test_set_localization
+ Setting.default_language = 'en'
+
+ # a french user
+ get 'projects', { }, 'Accept-Language' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Projets'
+ assert_equal :fr, current_language
+
+ # then an italien user
+ get 'projects', { }, 'Accept-Language' => 'it;q=0.8,en-us;q=0.5,en;q=0.3'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Progetti'
+ assert_equal :it, current_language
+
+ # not a supported language: default language should be used
+ get 'projects', { }, 'Accept-Language' => 'zz'
+ assert_response :success
+ assert_tag :tag => 'h2', :content => 'Projects'
+ end
+
+ def test_token_based_access_should_not_start_session
+ # issue of a private project
+ get 'issues/4.atom'
+ assert_response 302
+
+ rss_key = User.find(2).rss_key
+ get "issues/4.atom?key=#{rss_key}"
+ assert_response 200
+ assert_nil session[:user_id]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__)}/../test_helper"
+
+class IssuesTest < ActionController::IntegrationTest
+ fixtures :projects,
+ :users,
+ :roles,
+ :members,
+ :trackers,
+ :projects_trackers,
+ :enabled_modules,
+ :issue_statuses,
+ :issues,
+ :enumerations,
+ :custom_fields,
+ :custom_values,
+ :custom_fields_trackers
+
+ # create an issue
+ def test_add_issue
+ log_user('jsmith', 'jsmith')
+ get 'projects/1/issues/new', :tracker_id => '1'
+ assert_response :success
+ assert_template 'issues/new'
+
+ post 'projects/1/issues', :tracker_id => "1",
+ :issue => { :start_date => "2006-12-26",
+ :priority_id => "4",
+ :subject => "new test issue",
+ :category_id => "",
+ :description => "new issue",
+ :done_ratio => "0",
+ :due_date => "",
+ :assigned_to_id => "" },
+ :custom_fields => {'2' => 'Value for field 2'}
+ # find created issue
+ issue = Issue.find_by_subject("new test issue")
+ assert_kind_of Issue, issue
+
+ # check redirection
+ assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
+ follow_redirect!
+ assert_equal issue, assigns(:issue)
+
+ # check issue attributes
+ assert_equal 'jsmith', issue.author.login
+ assert_equal 1, issue.project.id
+ assert_equal 1, issue.status.id
+ end
+
+ # add then remove 2 attachments to an issue
+ def test_issue_attachements
+ log_user('jsmith', 'jsmith')
+ set_tmp_attachments_directory
+
+ post 'issues/1/edit',
+ :notes => 'Some notes',
+ :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
+ assert_redirected_to "issues/1"
+
+ # make sure attachment was saved
+ attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
+ assert_kind_of Attachment, attachment
+ assert_equal Issue.find(1), attachment.container
+ assert_equal 'This is an attachment', attachment.description
+ # verify the size of the attachment stored in db
+ #assert_equal file_data_1.length, attachment.filesize
+ # verify that the attachment was written to disk
+ assert File.exist?(attachment.diskfile)
+
+ # remove the attachments
+ Issue.find(1).attachments.each(&:destroy)
+ assert_equal 0, Issue.find(1).attachments.length
+ end
+
+ def test_other_formats_links_on_get_index
+ get '/projects/ecookbook/issues'
+
+ %w(Atom PDF CSV).each do |format|
+ assert_tag :a, :content => format,
+ :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
+ :rel => 'nofollow' }
+ end
+ end
+
+ def test_other_formats_links_on_post_index_without_project_id_in_url
+ post '/issues', :project_id => 'ecookbook'
+
+ %w(Atom PDF CSV).each do |format|
+ assert_tag :a, :content => format,
+ :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
+ :rel => 'nofollow' }
+ end
+ end
+
+ def test_pagination_links_on_get_index
+ Setting.per_page_options = '2'
+ get '/projects/ecookbook/issues'
+
+ assert_tag :a, :content => '2',
+ :attributes => { :href => '/projects/ecookbook/issues?page=2' }
+
+ end
+
+ def test_pagination_links_on_post_index_without_project_id_in_url
+ Setting.per_page_options = '2'
+ post '/issues', :project_id => 'ecookbook'
+
+ assert_tag :a, :content => '2',
+ :attributes => { :href => '/projects/ecookbook/issues?page=2' }
+
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require "#{File.dirname(__FILE__)}/../../../test_helper"
+
+class MenuManagerTest < ActionController::IntegrationTest
+ include Redmine::I18n
+
+ fixtures :all
+
+ def test_project_menu_with_specific_locale
+ get 'projects/ecookbook/issues', { }, 'Accept-Language' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_activity),
+ :attributes => { :href => '/projects/ecookbook/activity',
+ :class => 'activity' } } }
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_issue_plural),
+ :attributes => { :href => '/projects/ecookbook/issues',
+ :class => 'issues selected' } } }
+ end
+
+ def test_project_menu_with_additional_menu_items
+ Setting.default_language = 'en'
+ assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.push :foo, { :controller => 'projects', :action => 'show' }, :caption => 'Foo'
+ menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
+ menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
+ end
+
+ get 'projects/ecookbook'
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
+ :attributes => { :class => 'foo' } } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
+ :attributes => { :class => 'bar' } },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
+ :attributes => { :class => 'hello' } },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
+
+ # Remove the menu items
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.delete :foo
+ menu.delete :bar
+ menu.delete :hello
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__)}/../test_helper"
+
+class ProjectsTest < ActionController::IntegrationTest
+ fixtures :projects, :users, :members
+
+ def test_archive_project
+ subproject = Project.find(1).children.first
+ log_user("admin", "admin")
+ get "admin/projects"
+ assert_response :success
+ assert_template "admin/projects"
+ post "projects/archive", :id => 1
+ assert_redirected_to "admin/projects"
+ assert !Project.find(1).active?
+
+ get 'projects/1'
+ assert_response 403
+ get "projects/#{subproject.id}"
+ assert_response 403
+
+ post "projects/unarchive", :id => 1
+ assert_redirected_to "admin/projects"
+ assert Project.find(1).active?
+ get "projects/1"
+ assert_response :success
+ end
+end
--- /dev/null
+# Mocks out OpenID
+#
+# http://www.northpub.com/articles/2007/04/02/testing-openid-support
+module OpenIdAuthentication
+
+ EXTENSION_FIELDS = {'email' => 'user@somedomain.com',
+ 'nickname' => 'cool_user',
+ 'country' => 'US',
+ 'postcode' => '12345',
+ 'fullname' => 'Cool User',
+ 'dob' => '1970-04-01',
+ 'language' => 'en',
+ 'timezone' => 'America/New_York'}
+
+ protected
+
+ def authenticate_with_open_id(identity_url = params[:openid_url], options = {}) #:doc:
+ if User.find_by_identity_url(identity_url) || identity_url.include?('good')
+ # Don't process registration fields unless it is requested.
+ unless identity_url.include?('blank') || (options[:required].nil? && options[:optional].nil?)
+ extension_response_fields = {}
+
+ options[:required].each do |field|
+ extension_response_fields[field.to_s] = EXTENSION_FIELDS[field.to_s]
+ end unless options[:required].nil?
+
+ options[:optional].each do |field|
+ extension_response_fields[field.to_s] = EXTENSION_FIELDS[field.to_s]
+ end unless options[:optional].nil?
+ end
+
+ yield Result[:successful], identity_url , extension_response_fields
+ else
+ logger.info "OpenID authentication failed: #{identity_url}"
+ yield Result[:failed], identity_url, nil
+ end
+ end
+
+ private
+
+ def add_simple_registration_fields(open_id_response, fields)
+ open_id_response.add_extension_arg('sreg', 'required', [ fields[:required] ].flatten * ',') if fields[:required]
+ open_id_response.add_extension_arg('sreg', 'optional', [ fields[:optional] ].flatten * ',') if fields[:optional]
+ end
+end
--- /dev/null
+module ObjectDaddyHelpers
+ # TODO: The gem or official version of ObjectDaddy doesn't set
+ # protected attributes so they need to be wrapped.
+ def User.generate_with_protected!(attributes={})
+ user = User.spawn(attributes) do |user|
+ user.login = User.next_login
+ attributes.each do |attr,v|
+ user.send("#{attr}=", v)
+ end
+ end
+ user.save!
+ user
+ end
+
+ # Generate the default Query
+ def Query.generate_default!(attributes={})
+ query = Query.spawn(attributes)
+ query.name ||= '_'
+ query.save!
+ query
+ end
+
+ # Generate an issue for a project, using it's trackers
+ def Issue.generate_for_project!(project, attributes={})
+ issue = Issue.spawn(attributes) do |issue|
+ issue.project = project
+ end
+ issue.tracker = project.trackers.first unless project.trackers.empty?
+ issue.save!
+ issue
+ end
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ENV["RAILS_ENV"] ||= "test"
+require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
+require 'test_help'
+require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
+require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
+
+require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
+include ObjectDaddyHelpers
+
+class ActiveSupport::TestCase
+ # 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)
+ get "/login"
+ assert_equal nil, session[:user_id]
+ assert_response :success
+ assert_template "account/login"
+ post "/login", :username => login, :password => password
+ assert_equal login, User.find(session[:user_id]).login
+ end
+
+ def uploaded_test_file(name, mime)
+ ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
+ end
+
+ # Use a temporary directory for attachment related tests
+ def set_tmp_attachments_directory
+ Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
+ Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
+ Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
+ end
+
+ def with_settings(options, &block)
+ saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
+ options.each {|k, v| Setting[k] = v}
+ yield
+ saved_settings.each {|k, v| Setting[k] = v}
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+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
+
+ def setup
+ @project = Project.find(1)
+ end
+
+ def test_activity_without_subprojects
+ events = find_events(User.anonymous, :project => @project)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert !events.include?(Issue.find(4))
+ # subproject issue
+ assert !events.include?(Issue.find(5))
+ end
+
+ def test_activity_with_subprojects
+ events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # subproject issue
+ assert events.include?(Issue.find(5))
+ end
+
+ def test_global_activity_anonymous
+ events = find_events(User.anonymous)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert events.include?(Message.find(5))
+ # Issue of a private project
+ assert !events.include?(Issue.find(4))
+ end
+
+ def test_global_activity_logged_user
+ events = find_events(User.find(2)) # manager
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # Issue of a private project the user belongs to
+ assert events.include?(Issue.find(4))
+ end
+
+ def test_user_activity
+ user = User.find(2)
+ events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
+
+ assert(events.size > 0)
+ assert(events.size <= 10)
+ assert_nil(events.detect {|e| e.event_author != user})
+ end
+
+ def test_files_activity
+ f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
+ f.scope = ['files']
+ events = f.events
+
+ assert_kind_of Array, events
+ assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1))
+ assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1))
+ assert_equal [Attachment], events.collect(&:class).uniq
+ assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort
+ end
+
+ private
+
+ def find_events(user, options={})
+ Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
+ end
+end
--- /dev/null
+# encoding: utf-8
+#
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class AttachmentTest < ActiveSupport::TestCase
+ fixtures :issues, :users
+
+ def setup
+ 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 Digest::MD5.hexdigest(uploaded_test_file("testfile.txt", "text/plain").read), a.digest
+ assert File.exist?(a.diskfile)
+ 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
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class AuthSourceLdapTest < ActiveSupport::TestCase
+
+ def setup
+ end
+
+ def test_create
+ a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName')
+ assert a.save
+ end
+
+ def test_should_strip_ldap_attributes
+ a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName',
+ :attr_firstname => 'givenName ')
+ assert a.save
+ assert_equal 'givenName', a.reload.attr_firstname
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class BoardTest < ActiveSupport::TestCase
+ fixtures :projects, :boards, :messages
+
+ def setup
+ @project = Project.find(1)
+ end
+
+ def test_create
+ board = Board.new(:project => @project, :name => 'Test board', :description => 'Test board description')
+ assert board.save
+ board.reload
+ assert_equal 'Test board', board.name
+ assert_equal 'Test board description', board.description
+ assert_equal @project, board.project
+ assert_equal 0, board.topics_count
+ assert_equal 0, board.messages_count
+ assert_nil board.last_message
+ # last position
+ assert_equal @project.boards.size, board.position
+ end
+
+ def test_destroy
+ board = Board.find(1)
+ assert board.destroy
+ # make sure that the associated messages are removed
+ assert_equal 0, Message.count(:conditions => {:board_id => 1})
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class CalendarTest < ActiveSupport::TestCase
+
+ def test_monthly
+ c = Redmine::Helpers::Calendar.new(Date.today, :fr, :month)
+ assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday]
+
+ c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :month)
+ assert_equal ['2007-06-25'.to_date, '2007-08-05'.to_date], [c.startdt, c.enddt]
+
+ c = Redmine::Helpers::Calendar.new(Date.today, :en, :month)
+ assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday]
+ end
+
+ def test_weekly
+ c = Redmine::Helpers::Calendar.new(Date.today, :fr, :week)
+ assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday]
+
+ c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :week)
+ assert_equal ['2007-07-09'.to_date, '2007-07-15'.to_date], [c.startdt, c.enddt]
+
+ c = Redmine::Helpers::Calendar.new(Date.today, :en, :week)
+ assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday]
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTest < ActiveSupport::TestCase
+ fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
+
+ def setup
+ end
+
+ 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'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => 'New commit (#2). Fixes #1')
+ c.scan_comment_for_issue_ids
+
+ assert_equal [1, 2], c.issue_ids.sort
+ fixed = Issue.find(1)
+ assert fixed.closed?
+ assert_equal 90, fixed.done_ratio
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_ref_keywords_any_line_start
+ Setting.commit_ref_keywords = '*'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '#1 is the reason of this commit')
+ c.scan_comment_for_issue_ids
+
+ assert_equal [1], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_allow_brackets_around_a_issue_number
+ Setting.commit_ref_keywords = '*'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '[#1] Worked on this issue')
+ c.scan_comment_for_issue_ids
+
+ assert_equal [1], c.issue_ids.sort
+ end
+
+ def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
+ Setting.commit_ref_keywords = '*'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '[#1 #2, #3] Worked on these')
+ c.scan_comment_for_issue_ids
+
+ assert_equal [1,2,3], c.issue_ids.sort
+ end
+
+ def test_previous
+ changeset = Changeset.find_by_revision('3')
+ assert_equal Changeset.find_by_revision('2'), changeset.previous
+ end
+
+ def test_previous_nil
+ changeset = Changeset.find_by_revision('1')
+ assert_nil changeset.previous
+ end
+
+ def test_next
+ changeset = Changeset.find_by_revision('2')
+ assert_equal Changeset.find_by_revision('3'), changeset.next
+ end
+
+ def test_next_nil
+ changeset = Changeset.find_by_revision('10')
+ assert_nil changeset.next
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class CommentTest < ActiveSupport::TestCase
+ fixtures :users, :news, :comments
+
+ def setup
+ @jsmith = User.find(2)
+ @news = News.find(1)
+ end
+
+ def test_create
+ comment = Comment.new(:commented => @news, :author => @jsmith, :comments => "my comment")
+ assert comment.save
+ @news.reload
+ assert_equal 2, @news.comments_count
+ end
+
+ def test_validate
+ comment = Comment.new(:commented => @news)
+ assert !comment.save
+ assert_equal 2, comment.errors.length
+ end
+
+ def test_destroy
+ comment = Comment.find(1)
+ assert comment.destroy
+ @news.reload
+ assert_equal 0, @news.comments_count
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class CustomFieldTest < ActiveSupport::TestCase
+ fixtures :custom_fields
+
+ def test_create
+ field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
+ assert field.save
+ end
+
+ def test_possible_values_should_accept_an_array
+ field = CustomField.new
+ field.possible_values = ["One value", ""]
+ assert_equal ["One value"], field.possible_values
+ end
+
+ def test_possible_values_should_accept_a_string
+ field = CustomField.new
+ field.possible_values = "One value"
+ assert_equal ["One value"], field.possible_values
+ end
+
+ def test_possible_values_should_accept_a_multiline_string
+ field = CustomField.new
+ field.possible_values = "One value\nAnd another one \r\n \n"
+ assert_equal ["One value", "And another one"], field.possible_values
+ end
+
+ def test_destroy
+ field = CustomField.find(1)
+ assert field.destroy
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class CustomValueTest < ActiveSupport::TestCase
+ fixtures :custom_fields, :custom_values, :users
+
+ def test_string_field_validation_with_blank_value
+ f = CustomField.new(:field_format => 'string')
+ v = CustomValue.new(:custom_field => f)
+
+ v.value = nil
+ assert v.valid?
+ v.value = ''
+ assert v.valid?
+
+ f.is_required = true
+ v.value = nil
+ assert !v.valid?
+ v.value = ''
+ assert !v.valid?
+ end
+
+ def test_string_field_validation_with_min_and_max_lengths
+ f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
+ v = CustomValue.new(:custom_field => f, :value => '')
+ assert v.valid?
+ v.value = 'a'
+ assert !v.valid?
+ v.value = 'a' * 2
+ assert v.valid?
+ v.value = 'a' * 6
+ assert !v.valid?
+ end
+
+ def test_string_field_validation_with_regexp
+ f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
+ v = CustomValue.new(:custom_field => f, :value => '')
+ assert v.valid?
+ v.value = 'abc'
+ assert !v.valid?
+ v.value = 'ABC'
+ assert v.valid?
+ end
+
+ def test_date_field_validation
+ f = CustomField.new(:field_format => 'date')
+ v = CustomValue.new(:custom_field => f, :value => '')
+ assert v.valid?
+ v.value = 'abc'
+ assert !v.valid?
+ v.value = '1975-07-14'
+ assert v.valid?
+ end
+
+ def test_list_field_validation
+ f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
+ v = CustomValue.new(:custom_field => f, :value => '')
+ assert v.valid?
+ v.value = 'abc'
+ assert !v.valid?
+ v.value = 'value2'
+ assert v.valid?
+ end
+
+ def test_int_field_validation
+ f = CustomField.new(:field_format => 'int')
+ v = CustomValue.new(:custom_field => f, :value => '')
+ assert v.valid?
+ v.value = 'abc'
+ assert !v.valid?
+ v.value = '123'
+ assert v.valid?
+ v.value = '+123'
+ assert v.valid?
+ v.value = '-123'
+ assert v.valid?
+ end
+
+ def test_float_field_validation
+ v = CustomValue.new(:customized => User.find(:first), :custom_field => UserCustomField.find_by_name('Money'))
+ v.value = '11.2'
+ assert v.save
+ v.value = ''
+ assert v.save
+ v.value = '-6.250'
+ assert v.save
+ v.value = '6a'
+ assert !v.save
+ end
+
+ 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
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class DefaultDataTest < ActiveSupport::TestCase
+ include Redmine::I18n
+ fixtures :roles
+
+ def test_no_data
+ assert !Redmine::DefaultData::Loader::no_data?
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::no_data?
+ end
+
+ def test_load
+ valid_languages.each do |lang|
+ begin
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::load(lang)
+ assert_not_nil DocumentCategory.first
+ assert_not_nil IssuePriority.first
+ assert_not_nil TimeEntryActivity.first
+ rescue ActiveRecord::RecordInvalid => e
+ assert false, ":#{lang} default data is invalid (#{e.message})."
+ end
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class DocumentCategoryTest < ActiveSupport::TestCase
+ fixtures :enumerations, :documents, :issues
+
+ def test_should_be_an_enumeration
+ assert DocumentCategory.ancestors.include?(Enumeration)
+ end
+
+ def test_objects_count
+ assert_equal 1, DocumentCategory.find_by_name("Uncategorized").objects_count
+ assert_equal 0, DocumentCategory.find_by_name("User documentation").objects_count
+ end
+
+ def test_option_name
+ assert_equal :enumeration_doc_categories, DocumentCategory.new.option_name
+ end
+end
+
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class DocumentTest < ActiveSupport::TestCase
+ fixtures :projects, :enumerations, :documents
+
+ def test_create
+ doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
+ assert doc.save
+ end
+
+ def test_create_should_send_email_notification
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events << 'document_added'
+ doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
+
+ assert doc.save
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_create_with_default_category
+ # Sets a default category
+ e = Enumeration.find_by_name('Technical documentation')
+ e.update_attributes(:is_default => true)
+
+ doc = Document.new(:project => Project.find(1), :title => 'New document')
+ assert_equal e, doc.category
+ assert doc.save
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EnabledModuleTest < ActiveSupport::TestCase
+ fixtures :projects, :wikis
+
+ def test_enabling_wiki_should_create_a_wiki
+ CustomField.delete_all
+ project = Project.create!(:name => 'Project with wiki', :identifier => 'wikiproject')
+ assert_nil project.wiki
+ project.enabled_module_names = ['wiki']
+ project.reload
+ assert_not_nil project.wiki
+ assert_equal 'Wiki', project.wiki.start_page
+ end
+
+ def test_reenabling_wiki_should_not_create_another_wiki
+ project = Project.find(1)
+ assert_not_nil project.wiki
+ project.enabled_module_names = []
+ project.reload
+ assert_no_difference 'Wiki.count' do
+ project.enabled_module_names = ['wiki']
+ end
+ assert_not_nil project.wiki
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class EnumerationTest < ActiveSupport::TestCase
+ fixtures :enumerations, :issues, :custom_fields, :custom_values
+
+ def setup
+ end
+
+ def test_objects_count
+ # low priority
+ assert_equal 5, Enumeration.find(4).objects_count
+ # urgent
+ assert_equal 0, Enumeration.find(7).objects_count
+ end
+
+ def test_in_use
+ # low priority
+ assert Enumeration.find(4).in_use?
+ # urgent
+ assert !Enumeration.find(7).in_use?
+ end
+
+ def test_default
+ e = Enumeration.default
+ assert e.is_a?(Enumeration)
+ assert e.is_default?
+ assert_equal 'Default Enumeration', e.name
+ end
+
+ def test_create
+ e = Enumeration.new(:name => 'Not default', :is_default => false)
+ e.type = 'Enumeration'
+ assert e.save
+ assert_equal 'Default Enumeration', Enumeration.default.name
+ end
+
+ def test_create_as_default
+ e = Enumeration.new(:name => 'Very urgent', :is_default => true)
+ e.type = 'Enumeration'
+ assert e.save
+ assert_equal e, Enumeration.default
+ end
+
+ def test_update_default
+ e = Enumeration.default
+ e.update_attributes(:name => 'Changed', :is_default => true)
+ assert_equal e, Enumeration.default
+ end
+
+ def test_update_default_to_non_default
+ e = Enumeration.default
+ e.update_attributes(:name => 'Changed', :is_default => false)
+ assert_nil Enumeration.default
+ end
+
+ def test_change_default
+ e = Enumeration.find_by_name('Default Enumeration')
+ e.update_attributes(:name => 'Changed Enumeration', :is_default => true)
+ assert_equal e, Enumeration.default
+ end
+
+ def test_destroy_with_reassign
+ Enumeration.find(4).destroy(Enumeration.find(6))
+ assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
+ assert_equal 5, Enumeration.find(6).objects_count
+ end
+
+ def test_should_be_customizable
+ assert Enumeration.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
+ end
+
+ def test_should_belong_to_a_project
+ association = Enumeration.reflect_on_association(:project)
+ assert association, "No Project association found"
+ assert_equal :belongs_to, association.macro
+ end
+
+ def test_should_act_as_tree
+ enumeration = Enumeration.find(4)
+
+ assert enumeration.respond_to?(:parent)
+ assert enumeration.respond_to?(:children)
+ end
+
+ def test_is_override
+ # Defaults to off
+ enumeration = Enumeration.find(4)
+ assert !enumeration.is_override?
+
+ # Setup as an override
+ enumeration.parent = Enumeration.find(5)
+ assert enumeration.is_override?
+ end
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+
+class FilesystemAdapterTest < ActiveSupport::TestCase
+
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
+
+ if File.directory?(REPOSITORY_PATH)
+ def setup
+ @adapter = Redmine::Scm::Adapters::FilesystemAdapter.new(REPOSITORY_PATH)
+ end
+
+ def test_entries
+ assert_equal 2, @adapter.entries.size
+ assert_equal ["dir", "test"], @adapter.entries.collect(&:name)
+ assert_equal ["dir", "test"], @adapter.entries(nil).collect(&:name)
+ assert_equal ["dir", "test"], @adapter.entries("/").collect(&:name)
+ ["dir", "/dir", "/dir/", "dir/"].each do |path|
+ assert_equal ["subdir", "dirfile"], @adapter.entries(path).collect(&:name)
+ end
+ # If y try to use "..", the path is ignored
+ ["/../","dir/../", "..", "../", "/..", "dir/.."].each do |path|
+ assert_equal ["dir", "test"], @adapter.entries(path).collect(&:name), ".. must be ignored in path argument"
+ end
+ end
+
+ def test_cat
+ assert_equal "TEST CAT\n", @adapter.cat("test")
+ assert_equal "TEST CAT\n", @adapter.cat("/test")
+ # Revision number is ignored
+ assert_equal "TEST CAT\n", @adapter.cat("/test", 1)
+ end
+
+ else
+ puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
+ def test_fake; assert true end
+ end
+
+end
+
+
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class GitAdapterTest < ActiveSupport::TestCase
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
+
+ if File.directory?(REPOSITORY_PATH)
+ def setup
+ @adapter = Redmine::Scm::Adapters::GitAdapter.new(REPOSITORY_PATH)
+ end
+
+ def test_branches
+ assert_equal @adapter.branches, ['master', 'test_branch']
+ end
+
+ def test_getting_all_revisions
+ assert_equal 12, @adapter.revisions('',nil,nil,:all => true).length
+ end
+ else
+ puts "Git test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class GroupTest < ActiveSupport::TestCase
+ fixtures :all
+
+ def test_create
+ g = Group.new(:lastname => 'New group')
+ assert g.save
+ end
+
+ def test_roles_given_to_new_user
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+
+ Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
+ group.users << user
+ assert user.member_of?(project)
+ end
+
+ def test_roles_given_to_existing_user
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+
+ group.users << user
+ m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
+ assert user.member_of?(project)
+ end
+
+ def test_roles_updated
+ group = Group.find(11)
+ user = User.find(9)
+ project = Project.first
+ group.users << user
+ m = Member.create!(:principal => group, :project => project, :role_ids => [1])
+ assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [1, 2]
+ assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [2]
+ assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
+
+ m.role_ids = [1]
+ assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
+ end
+
+ def test_roles_removed_when_removing_group_membership
+ assert User.find(8).member_of?(Project.find(5))
+ Member.find_by_project_id_and_user_id(5, 10).destroy
+ assert !User.find(8).member_of?(Project.find(5))
+ end
+
+ def test_roles_removed_when_removing_user_from_group
+ assert User.find(8).member_of?(Project.find(5))
+ User.find(8).groups.clear
+ assert !User.find(8).member_of?(Project.find(5))
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class ApplicationHelperTest < HelperTestCase
+ include ApplicationHelper
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::DateHelper
+
+ fixtures :projects, :roles, :enabled_modules, :users,
+ :repositories, :changesets,
+ :trackers, :issue_statuses, :issues, :versions, :documents,
+ :wikis, :wiki_pages, :wiki_contents,
+ :boards, :messages,
+ :attachments
+
+ def setup
+ super
+ end
+
+ def test_auto_links
+ to_test = {
+ 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
+ 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
+ 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
+ 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
+ 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
+ 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
+ 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
+ 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
+ '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
+ '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
+ '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
+ '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
+ '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
+ '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
+ 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
+ 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>',
+ 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
+ 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
+ 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
+ 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
+ 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
+ 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
+ # two exclamation marks
+ 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_auto_mailto
+ assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
+ textilizable('test@foo.bar')
+ end
+
+ def test_inline_images
+ to_test = {
+ '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
+ 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
+ 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
+ # inline styles should be stripped
+ 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
+ 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
+ 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted "title"" alt="This is a double-quoted "title"" />',
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_inline_images_inside_tags
+ raw = <<-RAW
+h1. !foo.png! Heading
+
+Centered image:
+
+p=. !bar.gif!
+RAW
+
+ assert textilizable(raw).include?('<img src="foo.png" alt="" />')
+ assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
+ end
+
+ def test_acronyms
+ to_test = {
+ 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
+ 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted "title"">GPL</acronym>',
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+
+ end
+
+ def test_attached_images
+ to_test = {
+ 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
+ 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
+ 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
+ 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
+ # link image
+ '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
+ }
+ attachments = Attachment.find(:all)
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
+ end
+
+ def test_textile_external_links
+ to_test = {
+ 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
+ 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
+ '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
+ '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with "double-quotes"" class="external">link</a>',
+ "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
+ # no multiline link text
+ "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
+ # mailto link
+ "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
+ # two exclamation marks
+ '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_redmine_links
+ issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
+ :class => 'issue status-1 priority-1 overdue', :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},
+ :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
+
+ document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
+ :class => 'document')
+
+ version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
+ :class => 'version')
+
+ message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
+
+ source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
+ source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
+
+ to_test = {
+ # tickets
+ '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
+ # changesets
+ 'r1' => changeset_link,
+ 'r1.' => "#{changeset_link}.",
+ 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
+ 'r1,r2' => "#{changeset_link},#{changeset_link2}",
+ # documents
+ 'document#1' => document_link,
+ 'document:"Test document"' => document_link,
+ # versions
+ 'version#2' => version_link,
+ 'version:1.0' => version_link,
+ 'version:"1.0"' => version_link,
+ # source
+ 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
+ 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
+ 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
+ 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
+ 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
+ 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
+ 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
+ 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
+ # message
+ 'message#4' => link_to('Post 2', message_url, :class => 'message'),
+ 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
+ # escaping
+ '!#3.' => '#3.',
+ '!r1' => 'r1',
+ '!document#1' => 'document#1',
+ '!document:"Test document"' => 'document:"Test document"',
+ '!version#2' => 'version#2',
+ '!version:1.0' => 'version:1.0',
+ '!version:"1.0"' => 'version:"1.0"',
+ '!source:/some/file' => 'source:/some/file',
+ # invalid expressions
+ 'source:' => 'source:',
+ # url hash
+ "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
+ }
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_wiki_links
+ to_test = {
+ '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
+ '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
+ # link with anchor
+ '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
+ '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
+ # page that doesn't exist
+ '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
+ '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
+ # link to another project wiki
+ '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">onlinestore</a>',
+ '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">Wiki</a>',
+ '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
+ '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
+ '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
+ # striked through link
+ '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
+ '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
+ # escaping
+ '![[Another page|Page]]' => '[[Another page|Page]]',
+ # project does not exist
+ '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
+ '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
+ }
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_html_tags
+ to_test = {
+ "<div>content</div>" => "<p><div>content</div></p>",
+ "<div class=\"bold\">content</div>" => "<p><div class=\"bold\">content</div></p>",
+ "<script>some script;</script>" => "<p><script>some script;</script></p>",
+ # do not escape pre/code tags
+ "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
+ "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
+ "<pre><div>content</div></pre>" => "<pre><div>content</div></pre>",
+ "HTML comment: <!-- no comments -->" => "<p>HTML comment: <!-- no comments --></p>",
+ "<!-- opening comment" => "<p><!-- opening comment</p>",
+ # remove attributes except class
+ "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
+ "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
+ }
+ to_test.each { |text, result| assert_equal result, textilizable(text) }
+ end
+
+ def test_allowed_html_tags
+ to_test = {
+ "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
+ "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
+ "<notextile>this is <tag>a tag</tag></notextile>" => "this is <tag>a tag</tag>"
+ }
+ to_test.each { |text, result| assert_equal result, textilizable(text) }
+ end
+
+ def test_pre_tags
+ raw = <<-RAW
+Before
+
+<pre>
+<prepared-statement-cache-size>32</prepared-statement-cache-size>
+</pre>
+
+After
+RAW
+
+ expected = <<-EXPECTED
+<p>Before</p>
+<pre>
+<prepared-statement-cache-size>32</prepared-statement-cache-size>
+</pre>
+<p>After</p>
+EXPECTED
+
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
+ end
+
+ def test_syntax_highlight
+ raw = <<-RAW
+<pre><code class="ruby">
+# Some ruby code here
+</pre></code>
+RAW
+
+ expected = <<-EXPECTED
+<pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
+</pre></code>
+EXPECTED
+
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
+ end
+
+ def test_wiki_links_in_tables
+ to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
+ '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
+ '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
+ '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
+ }
+ @project = Project.find(1)
+ to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
+ end
+
+ def test_text_formatting
+ to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
+ '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
+ 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
+ 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
+ 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_wiki_horizontal_rule
+ assert_equal '<hr />', textilizable('---')
+ assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
+ end
+
+ def test_acronym
+ assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
+ textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
+ end
+
+ def test_footnotes
+ raw = <<-RAW
+This is some text[1].
+
+fn1. This is the foot note
+RAW
+
+ expected = <<-EXPECTED
+<p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
+<p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
+EXPECTED
+
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
+ end
+
+ def test_table_of_content
+ raw = <<-RAW
+{{toc}}
+
+h1. Title
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+
+h2. Subtitle with a [[Wiki]] link
+
+Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+
+h2. Subtitle with [[Wiki|another Wiki]] link
+
+h2. Subtitle with %{color:red}red text%
+
+h1. Another title
+
+RAW
+
+ expected = '<ul class="toc">' +
+ '<li class="heading1"><a href="#Title">Title</a></li>' +
+ '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
+ '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
+ '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
+ '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
+ '</ul>'
+
+ assert textilizable(raw).gsub("\n", "").include?(expected)
+ end
+
+ def test_blockquote
+ # orig raw text
+ raw = <<-RAW
+John said:
+> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+> Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+> * Donec odio lorem,
+> * sagittis ac,
+> * malesuada in,
+> * adipiscing eu, dolor.
+>
+> >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
+> Proin a tellus. Nam vel neque.
+
+He's right.
+RAW
+
+ # expected html
+ expected = <<-EXPECTED
+<p>John said:</p>
+<blockquote>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+<ul>
+ <li>Donec odio lorem,</li>
+ <li>sagittis ac,</li>
+ <li>malesuada in,</li>
+ <li>adipiscing eu, dolor.</li>
+</ul>
+<blockquote>
+<p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
+</blockquote>
+<p>Proin a tellus. Nam vel neque.</p>
+</blockquote>
+<p>He's right.</p>
+EXPECTED
+
+ assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
+ end
+
+ def test_table
+ raw = <<-RAW
+This is a table with empty cells:
+
+|cell11|cell12||
+|cell21||cell23|
+|cell31|cell32|cell33|
+RAW
+
+ expected = <<-EXPECTED
+<p>This is a table with empty cells:</p>
+
+<table>
+ <tr><td>cell11</td><td>cell12</td><td></td></tr>
+ <tr><td>cell21</td><td></td><td>cell23</td></tr>
+ <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
+</table>
+EXPECTED
+
+ assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
+ end
+
+ def test_table_with_line_breaks
+ raw = <<-RAW
+This is a table with line breaks:
+
+|cell11
+continued|cell12||
+|-cell21-||cell23
+cell23 line2
+cell23 *line3*|
+|cell31|cell32
+cell32 line2|cell33|
+
+RAW
+
+ expected = <<-EXPECTED
+<p>This is a table with line breaks:</p>
+
+<table>
+ <tr>
+ <td>cell11<br />continued</td>
+ <td>cell12</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td><del>cell21</del></td>
+ <td></td>
+ <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
+ </tr>
+ <tr>
+ <td>cell31</td>
+ <td>cell32<br/>cell32 line2</td>
+ <td>cell33</td>
+ </tr>
+</table>
+EXPECTED
+
+ assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
+ end
+
+ def test_default_formatter
+ Setting.text_formatting = 'unknown'
+ text = 'a *link*: http://www.example.net/'
+ assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
+ Setting.text_formatting = 'textile'
+ end
+
+ def test_due_date_distance_in_words
+ to_test = { Date.today => 'Due in 0 days',
+ Date.today + 1 => 'Due in 1 day',
+ Date.today + 100 => 'Due in about 3 months',
+ Date.today + 20000 => 'Due in over 54 years',
+ Date.today - 1 => '1 day late',
+ Date.today - 100 => 'about 3 months late',
+ Date.today - 20000 => 'over 54 years late',
+ }
+ to_test.each do |date, expected|
+ assert_equal expected, due_date_distance_in_words(date)
+ end
+ end
+
+ def test_avatar
+ # turn on avatars
+ Setting.gravatar_enabled = '1'
+ assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
+ assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
+ assert_nil avatar('jsmith')
+ assert_nil avatar(nil)
+
+ # turn off avatars
+ Setting.gravatar_enabled = '0'
+ assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
+ end
+
+ def test_link_to_user
+ user = User.find(2)
+ t = link_to_user(user)
+ assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
+ end
+
+ def test_link_to_user_should_not_link_to_locked_user
+ user = User.find(5)
+ assert user.locked?
+ t = link_to_user(user)
+ assert_equal user.name, t
+ end
+
+ def test_link_to_user_should_not_link_to_anonymous
+ user = User.anonymous
+ assert user.anonymous?
+ t = link_to_user(user)
+ assert_equal ::I18n.t(:label_user_anonymous), t
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class CustomFieldsHelperTest < HelperTestCase
+ include CustomFieldsHelper
+ include Redmine::I18n
+
+ def test_format_boolean_value
+ I18n.locale = 'en'
+ assert_equal 'Yes', format_value('1', 'bool')
+ assert_equal 'No', format_value('0', 'bool')
+ end
+end
--- /dev/null
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class SearchHelperTest < HelperTestCase
+ include SearchHelper
+
+ def test_highlight_single_token
+ assert_equal 'This is a <span class="highlight token-0">token</span>.',
+ highlight_tokens('This is a token.', %w(token))
+ end
+
+ def test_highlight_multiple_tokens
+ assert_equal 'This is a <span class="highlight token-0">token</span> and <span class="highlight token-1">another</span> <span class="highlight token-0">token</span>.',
+ highlight_tokens('This is a token and another token.', %w(token another))
+ end
+
+ def test_highlight_should_not_exceed_maximum_length
+ s = (('1234567890' * 100) + ' token ') * 100
+ r = highlight_tokens(s, %w(token))
+ assert r.include?('<span class="highlight token-0">token</span>')
+ assert r.length <= 1300
+ end
+
+ def test_highlight_multibyte
+ s = ('й' * 200) + ' token ' + ('й' * 200)
+ r = highlight_tokens(s, %w(token))
+ assert_equal ('й' * 45) + ' ... ' + ('й' * 44) + ' <span class="highlight token-0">token</span> ' + ('й' * 44) + ' ... ' + ('й' * 45), r
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class SortHelperTest < HelperTestCase
+ include SortHelper
+
+ def setup
+ @session = nil
+ @sort_param = nil
+ end
+
+ def test_default_sort_clause_with_array
+ sort_init 'attr1', 'desc'
+ sort_update(['attr1', 'attr2'])
+
+ 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
+ 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
+ end
+
+ def test_params_sort
+ @sort_param = 'attr1,attr2:desc'
+
+ sort_init 'attr1', 'desc'
+ sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
+
+ assert_equal 'table1.attr1, table2.attr2 DESC', sort_clause
+ assert_equal 'attr1,attr2:desc', @session['foo_bar_sort']
+ end
+
+ def test_invalid_params_sort
+ @sort_param = 'invalid_key'
+
+ sort_init 'attr1', 'desc'
+ sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
+
+ assert_equal 'table1.attr1 DESC', sort_clause
+ assert_equal 'attr1:desc', @session['foo_bar_sort']
+ end
+
+ def test_invalid_order_params_sort
+ @sort_param = 'attr1:foo:bar,attr2'
+
+ sort_init 'attr1', 'desc'
+ sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
+
+ assert_equal 'table1.attr1, table2.attr2', sort_clause
+ assert_equal 'attr1,attr2', @session['foo_bar_sort']
+ end
+
+ private
+
+ def controller_name; 'foo'; end
+ def action_name; 'bar'; end
+ def params; {:sort => @sort_param}; end
+ def session; @session ||= {}; end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class TimelogHelperTest < HelperTestCase
+ include TimelogHelper
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::DateHelper
+
+ fixtures :projects, :roles, :enabled_modules, :users,
+ :repositories, :changesets,
+ :trackers, :issue_statuses, :issues, :versions, :documents,
+ :wikis, :wiki_pages, :wiki_contents,
+ :boards, :messages,
+ :attachments,
+ :enumerations
+
+ def setup
+ super
+ end
+
+ def test_activities_collection_for_select_options_should_return_array_of_activity_names_and_ids
+ activities = activity_collection_for_select_options
+ assert activities.include?(["Design", 9])
+ assert activities.include?(["Development", 10])
+ end
+
+ def test_activities_collection_for_select_options_should_not_include_inactive_activities
+ activities = activity_collection_for_select_options
+ assert !activities.include?(["Inactive Activity", 14])
+ end
+
+ def test_activities_collection_for_select_options_should_use_the_projects_override
+ project = Project.find(1)
+ override_activity = TimeEntryActivity.create!({:name => "Design override", :parent => TimeEntryActivity.find_by_name("Design"), :project => project})
+
+ activities = activity_collection_for_select_options(nil, project)
+ assert !activities.include?(["Design", 9]), "System activity found in: " + activities.inspect
+ assert activities.include?(["Design override", override_activity.id]), "Override activity not found in: " + activities.inspect
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class IssueCategoryTest < ActiveSupport::TestCase
+ fixtures :issue_categories, :issues
+
+ def setup
+ @category = IssueCategory.find(1)
+ end
+
+ def test_destroy
+ issue = @category.issues.first
+ @category.destroy
+ # Make sure the category was nullified on the issue
+ assert_nil issue.reload.category
+ end
+
+ def test_destroy_with_reassign
+ issue = @category.issues.first
+ reassign_to = IssueCategory.find(2)
+ @category.destroy(reassign_to)
+ # Make sure the issue was reassigned
+ assert_equal reassign_to, issue.reload.category
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class IssuePriorityTest < ActiveSupport::TestCase
+ fixtures :enumerations, :issues
+
+ def test_should_be_an_enumeration
+ assert IssuePriority.ancestors.include?(Enumeration)
+ end
+
+ def test_objects_count
+ # low priority
+ assert_equal 5, IssuePriority.find(4).objects_count
+ # urgent
+ assert_equal 0, IssuePriority.find(7).objects_count
+ end
+
+ def test_option_name
+ assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
+ end
+end
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class IssueStatusTest < ActiveSupport::TestCase
+ fixtures :issue_statuses, :issues
+
+ def test_create
+ status = IssueStatus.new :name => "Assigned"
+ assert !status.save
+ # status name uniqueness
+ assert_equal 1, status.errors.count
+
+ status.name = "Test Status"
+ assert status.save
+ assert !status.is_default
+ end
+
+ def test_destroy
+ count_before = IssueStatus.count
+ status = IssueStatus.find(3)
+ assert status.destroy
+ assert_equal count_before - 1, IssueStatus.count
+ end
+
+ def test_destroy_status_in_use
+ # Status assigned to an Issue
+ status = Issue.find(1).status
+ assert_raise(RuntimeError, "Can't delete status") { status.destroy }
+ end
+
+ def test_default
+ status = IssueStatus.default
+ assert_kind_of IssueStatus, status
+ end
+
+ def test_change_default
+ status = IssueStatus.find(2)
+ assert !status.is_default
+ status.is_default = true
+ assert status.save
+ status.reload
+
+ assert_equal status, IssueStatus.default
+ assert !IssueStatus.find(1).is_default
+ end
+
+ def test_reorder_should_not_clear_default_status
+ status = IssueStatus.default
+ status.move_to_bottom
+ status.reload
+ assert status.is_default?
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class IssueTest < ActiveSupport::TestCase
+ fixtures :projects, :users, :members, :member_roles, :roles,
+ :trackers, :projects_trackers,
+ :versions,
+ :issue_statuses, :issue_categories, :issue_relations, :workflows,
+ :enumerations,
+ :issues,
+ :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
+ :time_entries
+
+ def test_create
+ 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?
+ end
+
+ def test_create_with_required_custom_field
+ 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 I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
+ # Blank value
+ issue.custom_field_values = { field.id => '' }
+ assert !issue.save
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
+ # Invalid value
+ issue.custom_field_values = { field.id => 'SQLServer' }
+ assert !issue.save
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
+ # 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_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?}
+ # Anonymous user should not see issues without permission
+ Role.anonymous.remove_permission!(:view_issues)
+ issues = Issue.visible(User.anonymous).all
+ assert issues.empty?
+ end
+
+ def test_visible_scope_for_user
+ 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?}
+ # Non member user should not see issues without permission
+ Role.non_member.remove_permission!(:view_issues)
+ user.reload
+ issues = Issue.visible(user).all
+ assert issues.empty?
+ # User should see issues of projects for which he has view_issues permissions only
+ Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
+ user.reload
+ issues = Issue.visible(user).all
+ assert issues.any?
+ assert_nil issues.detect {|issue| issue.project_id != 2}
+ 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?}
+ 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_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_copy
+ issue = Issue.new.copy_from(1)
+ assert issue.save
+ issue.reload
+ orig = Issue.find(1)
+ assert_equal orig.subject, issue.subject
+ assert_equal orig.tracker, issue.tracker
+ assert_equal orig.custom_values.first.value, issue.custom_values.first.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_should_close_duplicates
+ # Create 3 issues
+ issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
+ assert issue1.save
+ issue2 = issue1.clone
+ assert issue2.save
+ issue3 = issue1.clone
+ assert issue3.save
+
+ # 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.find(:first), "Closing issue1")
+ issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
+ 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
+ # Create 3 issues
+ issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
+ assert issue1.save
+ issue2 = issue1.clone
+ assert issue2.save
+
+ # 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.find(:first), "Closing issue2")
+ issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
+ 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_nil issue.errors.on(: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_nil issue.errors.on(: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_nil issue.errors.on_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_move_to_another_project_with_same_category
+ issue = Issue.find(1)
+ assert issue.move_to(Project.find(2))
+ 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)
+ assert issue.move_to(Project.find(2))
+ issue.reload
+ assert_equal 2, issue.project_id
+ # Category cleared
+ assert_nil issue.category_id
+ end
+
+ def test_copy_to_the_same_project
+ issue = Issue.find(1)
+ copy = nil
+ assert_difference 'Issue.count' do
+ copy = issue.move_to(issue.project, nil, :copy => true)
+ 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 = nil
+ assert_difference 'Issue.count' do
+ copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
+ end
+ 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
+
+ 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_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_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})).overdue?
+ end
+
+ def test_assignable_users
+ assert_kind_of User, Issue.find(1).assignable_users.first
+ 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
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class JournalTest < ActiveSupport::TestCase
+ fixtures :issues, :issue_statuses, :journals, :journal_details
+
+ def setup
+ @journal = Journal.find 1
+ end
+
+ def test_journalized_is_an_issue
+ issue = @journal.issue
+ assert_kind_of Issue, issue
+ assert_equal 1, issue.id
+ end
+
+ def test_new_status
+ status = @journal.new_status
+ assert_not_nil status
+ assert_kind_of IssueStatus, status
+ assert_equal 2, status.id
+ end
+
+ def test_create_should_send_email_notification
+ ActionMailer::Base.deliveries.clear
+ issue = Issue.find(:first)
+ user = User.find(:first)
+ journal = issue.init_journal(user, issue)
+
+ assert journal.save
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::AccessControlTest < ActiveSupport::TestCase
+
+ def setup
+ @access_module = Redmine::AccessControl
+ end
+
+ def test_permissions
+ perms = @access_module.permissions
+ assert perms.is_a?(Array)
+ assert perms.first.is_a?(Redmine::AccessControl::Permission)
+ end
+
+ def test_module_permission
+ perm = @access_module.permission(:view_issues)
+ assert perm.is_a?(Redmine::AccessControl::Permission)
+ assert_equal :view_issues, perm.name
+ assert_equal :issue_tracking, perm.project_module
+ assert perm.actions.is_a?(Array)
+ assert perm.actions.include?('issues/index')
+ end
+
+ def test_no_module_permission
+ perm = @access_module.permission(:edit_project)
+ assert perm.is_a?(Redmine::AccessControl::Permission)
+ assert_equal :edit_project, perm.name
+ assert_nil perm.project_module
+ assert perm.actions.is_a?(Array)
+ assert perm.actions.include?('projects/settings')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::Hook::ManagerTest < ActiveSupport::TestCase
+
+ fixtures :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
+ @hook_helper = TestHookHelperController.new
+ @view_hook_helper = TestHookHelperView.new(RAILS_ROOT + '/app/views')
+ 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, 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 ['<a href="/issues">Issues</a>'], @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.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.deliver_issue_add(issue)
+ mail2 = ActionMailer::Base.deliveries.last
+
+ assert_equal mail.body, mail2.body
+ end
+end
+
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::I18nTest < ActiveSupport::TestCase
+ include Redmine::I18n
+ include ActionView::Helpers::NumberHelper
+
+ def setup
+ @hook_module = Redmine::Hook
+ end
+
+ def test_date_format_default
+ set_language_if_valid 'en'
+ today = Date.today
+ Setting.date_format = ''
+ assert_equal I18n.l(today), format_date(today)
+ end
+
+ def test_date_format
+ set_language_if_valid 'en'
+ today = Date.today
+ Setting.date_format = '%d %m %Y'
+ assert_equal today.strftime('%d %m %Y'), format_date(today)
+ end
+
+ def test_date_and_time_for_each_language
+ Setting.date_format = ''
+ valid_languages.each do |lang|
+ set_language_if_valid lang
+ assert_nothing_raised "#{lang} failure" do
+ format_date(Date.today)
+ format_time(Time.now)
+ format_time(Time.now, false)
+ assert_not_equal 'default', ::I18n.l(Date.today, :format => :default), "date.formats.default missing in #{lang}"
+ assert_not_equal 'time', ::I18n.l(Time.now, :format => :time), "time.formats.time missing in #{lang}"
+ end
+ assert l('date.day_names').is_a?(Array)
+ assert_equal 7, l('date.day_names').size
+
+ assert l('date.month_names').is_a?(Array)
+ assert_equal 13, l('date.month_names').size
+ end
+ end
+
+ def test_time_format_default
+ set_language_if_valid 'en'
+ now = Time.now
+ Setting.date_format = ''
+ Setting.time_format = ''
+ assert_equal I18n.l(now), format_time(now)
+ assert_equal I18n.l(now, :format => :time), format_time(now, false)
+ 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.utc
+ Setting.date_format = '%d %m %Y'
+ Setting.time_format = '%H %M'
+ assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
+ assert_equal Time.now.strftime('%H %M'), format_time(now, false)
+ end
+
+ def test_number_to_human_size_for_each_language
+ valid_languages.each do |lang|
+ set_language_if_valid lang
+ assert_nothing_raised "#{lang} failure" do
+ number_to_human_size(1024*1024*4)
+ end
+ end
+ end
+
+ def test_valid_languages
+ assert valid_languages.is_a?(Array)
+ assert valid_languages.first.is_a?(Symbol)
+ end
+
+ def test_valid_language
+ to_test = {'fr' => :fr,
+ 'Fr' => :fr,
+ 'zh' => :zh,
+ 'zh-tw' => :"zh-TW",
+ 'zh-TW' => :"zh-TW",
+ 'zh-ZZ' => nil }
+
+ to_test.each {|lang, expected| assert_equal expected, find_language(lang)}
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+class Redmine::MenuManager::MapperTest < Test::Unit::TestCase
+ context "Mapper#initialize" do
+ should "be tested"
+ end
+
+ def test_push_onto_root
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+ menu_mapper.exists?(:test_overview)
+ end
+
+ def test_push_onto_parent
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
+
+ assert menu_mapper.exists?(:test_child)
+ assert_equal :test_child, menu_mapper.find(:test_child).name
+ end
+
+ def test_push_onto_grandparent
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
+ menu_mapper.push :test_grandchild, { :controller => 'projects', :action => 'show'}, {:parent => :test_child}
+
+ assert menu_mapper.exists?(:test_grandchild)
+ grandchild = menu_mapper.find(:test_grandchild)
+ assert_equal :test_grandchild, grandchild.name
+ assert_equal :test_child, grandchild.parent.name
+ end
+
+ def test_push_first
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {:first => true}
+
+ root = menu_mapper.find(:root)
+ assert_equal 5, root.children.size
+ {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+ assert_not_nil root.children[position]
+ assert_equal name, root.children[position].name
+ end
+
+ end
+
+ def test_push_before
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {:before => :test_fourth}
+
+ root = menu_mapper.find(:root)
+ assert_equal 5, root.children.size
+ {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+ assert_not_nil root.children[position]
+ assert_equal name, root.children[position].name
+ end
+
+ end
+
+ def test_push_after
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {:after => :test_third}
+
+
+ root = menu_mapper.find(:root)
+ assert_equal 5, root.children.size
+ {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+ assert_not_nil root.children[position]
+ assert_equal name, root.children[position].name
+ end
+
+ end
+
+ def test_push_last
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {:last => true}
+ menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+
+ root = menu_mapper.find(:root)
+ assert_equal 5, root.children.size
+ {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+ assert_not_nil root.children[position]
+ assert_equal name, root.children[position].name
+ end
+
+ end
+
+ def test_exists_for_child_node
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+ menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview }
+
+ assert menu_mapper.exists?(:test_child)
+ end
+
+ def test_exists_for_invalid_node
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+ assert !menu_mapper.exists?(:nothing)
+ end
+
+ def test_find
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+ item = menu_mapper.find(:test_overview)
+ assert_equal :test_overview, item.name
+ assert_equal({:controller => 'projects', :action => 'show'}, item.url)
+ end
+
+ def test_find_missing
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+ item = menu_mapper.find(:nothing)
+ assert_equal nil, item
+ end
+
+ def test_delete
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+ assert_not_nil menu_mapper.delete(:test_overview)
+
+ assert_nil menu_mapper.find(:test_overview)
+ end
+
+ def test_delete_missing
+ menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+ assert_nil menu_mapper.delete(:test_missing)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+
+
+class Redmine::MenuManager::MenuHelperTest < HelperTestCase
+ include Redmine::MenuManager::MenuHelper
+ include ActionController::Assertions::SelectorAssertions
+ fixtures :users, :members, :projects, :enabled_modules
+
+ # Used by assert_select
+ def html_document
+ HTML::Document.new(@response.body)
+ end
+
+ def setup
+ super
+ @response = ActionController::TestResponse.new
+ # Stub the current menu item in the controller
+ def @controller.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', { })
+ @response.body = 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', { })
+ @response.body = 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', { })
+
+ @response.body = 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
+ }
+ })
+ @response.body = 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
+ }
+ })
+
+ @response.body = 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
+ @response.body = 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
+ @response.body = 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
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+module RedmineMenuTestHelper
+ # Helpers
+ def get_menu_item(menu_name, item_name)
+ Redmine::MenuManager.items(menu_name).find {|item| item.name == item_name.to_sym}
+ end
+end
+
+class Redmine::MenuManager::MenuItemTest < Test::Unit::TestCase
+ include RedmineMenuTestHelper
+
+ Redmine::MenuManager.map :test_menu do |menu|
+ menu.push(:parent, '/test', { })
+ menu.push(:child_menu, '/test', { :parent => :parent})
+ menu.push(:child2_menu, '/test', { :parent => :parent})
+ end
+
+ context "MenuItem#caption" do
+ should "be tested"
+ end
+
+ context "MenuItem#html_options" do
+ should "be tested"
+ end
+
+ # context new menu item
+ def test_new_menu_item_should_require_a_name
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new
+ end
+ end
+
+ def test_new_menu_item_should_require_an_url
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_missing_url)
+ end
+ end
+
+ def test_new_menu_item_should_require_the_options
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_missing_options, '/test')
+ end
+ end
+
+ def test_new_menu_item_with_all_required_parameters
+ assert Redmine::MenuManager::MenuItem.new(:test_good_menu, '/test', {})
+ end
+
+ def test_new_menu_item_should_require_a_proc_to_use_for_the_if_condition
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+ {
+ :if => ['not_a_proc']
+ })
+ end
+
+ assert Redmine::MenuManager::MenuItem.new(:test_good_if, '/test',
+ {
+ :if => Proc.new{}
+ })
+ end
+
+ def test_new_menu_item_should_allow_a_hash_for_extra_html_options
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+ {
+ :html => ['not_a_hash']
+ })
+ end
+
+ assert Redmine::MenuManager::MenuItem.new(:test_good_html, '/test',
+ {
+ :html => { :onclick => 'doSomething'}
+ })
+ end
+
+ def test_new_menu_item_should_require_a_proc_to_use_the_children_option
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+ {
+ :children => ['not_a_proc']
+ })
+ end
+
+ assert Redmine::MenuManager::MenuItem.new(:test_good_children, '/test',
+ {
+ :children => Proc.new{}
+ })
+ end
+
+ def test_new_should_not_allow_setting_the_parent_item_to_the_current_item
+ assert_raises ArgumentError do
+ Redmine::MenuManager::MenuItem.new(:test_error, '/test', { :parent => :test_error })
+ end
+ end
+
+ def test_has_children
+ parent_item = get_menu_item(:test_menu, :parent)
+ assert parent_item.hasChildren?
+ assert_equal 2, parent_item.children.size
+ assert_equal get_menu_item(:test_menu, :child_menu), parent_item.children[0]
+ assert_equal get_menu_item(:test_menu, :child2_menu), parent_item.children[1]
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::MenuManagerTest < Test::Unit::TestCase
+ context "MenuManager#map" do
+ should "be tested"
+ end
+
+ context "MenuManager#items" do
+ should "be tested"
+ end
+
+ should "be tested" do
+ assert true
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::MimeTypeTest < ActiveSupport::TestCase
+
+ def test_of
+ to_test = {'test.unk' => nil,
+ 'test.txt' => 'text/plain',
+ 'test.c' => 'text/x-c',
+ }
+ to_test.each do |name, expected|
+ assert_equal expected, Redmine::MimeType.of(name)
+ end
+ end
+
+ def test_css_class_of
+ to_test = {'test.unk' => nil,
+ 'test.txt' => 'text-plain',
+ 'test.c' => 'text-x-c',
+ }
+ to_test.each do |name, expected|
+ assert_equal expected, Redmine::MimeType.css_class_of(name)
+ end
+ end
+
+ def test_main_mimetype_of
+ to_test = {'test.unk' => nil,
+ 'test.txt' => 'text',
+ 'test.c' => 'text',
+ }
+ to_test.each do |name, expected|
+ assert_equal expected, Redmine::MimeType.main_mimetype_of(name)
+ end
+ end
+
+ def test_is_type
+ to_test = {['text', 'test.unk'] => false,
+ ['text', 'test.txt'] => true,
+ ['text', 'test.c'] => true,
+ }
+ to_test.each do |args, expected|
+ assert_equal expected, Redmine::MimeType.is_type?(*args)
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::PluginTest < ActiveSupport::TestCase
+
+ def setup
+ @klass = Redmine::Plugin
+ # In case some real plugins are installed
+ @klass.clear
+ end
+
+ def teardown
+ @klass.clear
+ end
+
+ def test_register
+ @klass.register :foo do
+ name 'Foo plugin'
+ url 'http://example.net/plugins/foo'
+ author 'John Smith'
+ author_url 'http://example.net/jsmith'
+ description 'This is a test plugin'
+ version '0.0.1'
+ settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings'
+ end
+
+ assert_equal 1, @klass.all.size
+
+ plugin = @klass.find('foo')
+ assert plugin.is_a?(Redmine::Plugin)
+ assert_equal :foo, plugin.id
+ assert_equal 'Foo plugin', plugin.name
+ assert_equal 'http://example.net/plugins/foo', plugin.url
+ assert_equal 'John Smith', plugin.author
+ assert_equal 'http://example.net/jsmith', plugin.author_url
+ assert_equal 'This is a test plugin', plugin.description
+ assert_equal '0.0.1', plugin.version
+ end
+
+ def test_requires_redmine
+ test = self
+ version = Redmine::VERSION.to_a.slice(0,3).join('.')
+
+ @klass.register :foo do
+ test.assert requires_redmine(:version_or_higher => '0.1.0')
+ test.assert requires_redmine(:version_or_higher => version)
+ test.assert requires_redmine(version)
+ test.assert_raise Redmine::PluginRequirementError do
+ requires_redmine(:version_or_higher => '99.0.0')
+ end
+
+ test.assert requires_redmine(:version => version)
+ test.assert requires_redmine(:version => [version, '99.0.0'])
+ test.assert_raise Redmine::PluginRequirementError do
+ requires_redmine(:version => '99.0.0')
+ end
+ test.assert_raise Redmine::PluginRequirementError do
+ requires_redmine(:version => ['98.0.0', '99.0.0'])
+ end
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::UnifiedDiffTest < ActiveSupport::TestCase
+
+ def setup
+ end
+
+ def test_subversion_diff
+ diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff'))
+ # number of files
+ assert_equal 4, diff.size
+ assert diff.detect {|file| file.file_name =~ %r{^config/settings.yml}}
+ end
+
+ def test_truncate_diff
+ diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff'), :max_lines => 20)
+ assert_equal 2, diff.size
+ end
+
+ def test_line_starting_with_dashes
+ diff = Redmine::UnifiedDiff.new(<<-DIFF
+--- old.txt Wed Nov 11 14:24:58 2009
++++ new.txt Wed Nov 11 14:25:02 2009
+@@ -1,8 +1,4 @@
+-Lines that starts with dashes:
+-
+-------------------------
+--- file.c
+-------------------------
++A line that starts with dashes:
+
+ and removed.
+
+@@ -23,4 +19,4 @@
+
+
+
+-Another chunk of change
++Another chunk of changes
+
+DIFF
+ )
+ assert_equal 1, diff.size
+ end
+
+ private
+
+ def read_diff_fixture(filename)
+ File.new(File.join(File.dirname(__FILE__), '/../../../fixtures/diffs', filename)).read
+ end
+end
--- /dev/null
+# Redmine - project management software\r
+# Copyright (C) 2006-2009 Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\r
+\r
+require File.dirname(__FILE__) + '/../../../test_helper'\r
+\r
+class Redmine::WikiFormattingTest < ActiveSupport::TestCase\r
+ \r
+ def test_should_link_urls_and_email_addresses\r
+ raw = <<-DIFF\r
+This is a sample *text* with a link: http://www.redmine.org\r
+and an email address foo@example.net\r
+DIFF\r
+\r
+ expected = <<-EXPECTED\r
+<p>This is a sample *text* with a link: <a href="http://www.redmine.org">http://www.redmine.org</a><br />\r
+and an email address <a href="mailto:foo@example.net">foo@example.net</a></p>\r
+EXPECTED\r
+\r
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), Redmine::WikiFormatting::NullFormatter::Formatter.new(raw).to_html.gsub(%r{[\r\n\t]}, '')\r
+ end\r
+end\r
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../../../../test_helper'
+
+class Redmine::WikiFormatting::MacrosTest < HelperTestCase
+ include ApplicationHelper
+ include ActionView::Helpers::TextHelper
+ fixtures :projects, :roles, :enabled_modules, :users,
+ :repositories, :changesets,
+ :trackers, :issue_statuses, :issues,
+ :versions, :documents,
+ :wikis, :wiki_pages, :wiki_contents,
+ :boards, :messages,
+ :attachments
+
+ def setup
+ super
+ @project = nil
+ end
+
+ def teardown
+ end
+
+ def test_macro_hello_world
+ text = "{{hello_world}}"
+ assert textilizable(text).match(/Hello world!/)
+ # escaping
+ text = "!{{hello_world}}"
+ assert_equal '<p>{{hello_world}}</p>', textilizable(text)
+ end
+
+ def test_macro_include
+ @project = Project.find(1)
+ # include a page of the current project wiki
+ text = "{{include(Another page)}}"
+ assert textilizable(text).match(/This is a link to a ticket/)
+
+ @project = nil
+ # include a page of a specific project wiki
+ text = "{{include(ecookbook:Another page)}}"
+ assert textilizable(text).match(/This is a link to a ticket/)
+
+ text = "{{include(ecookbook:)}}"
+ assert textilizable(text).match(/CookBook documentation/)
+
+ text = "{{include(unknowidentifier:somepage)}}"
+ assert textilizable(text).match(/Page not found/)
+ end
+
+ def test_macro_child_pages
+ expected = "<p><ul class=\"pages-hierarchy\">\n" +
+ "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
+ "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
+ "</ul>\n</p>"
+
+ @project = Project.find(1)
+ # child pages of the current wiki page
+ assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
+ # child pages of another page
+ assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
+
+ @project = Project.find(2)
+ assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
+ end
+
+ def test_macro_child_pages_with_option
+ expected = "<p><ul class=\"pages-hierarchy\">\n" +
+ "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
+ "<ul class=\"pages-hierarchy\">\n" +
+ "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
+ "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
+ "</ul>\n</li>\n</ul>\n</p>"
+
+ @project = Project.find(1)
+ # child pages of the current wiki page
+ assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
+ # child pages of another page
+ assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
+
+ @project = Project.find(2)
+ assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+module RedmineMenuTestHelper
+ # Assertions
+ def assert_number_of_items_in_menu(menu_name, count)
+ assert Redmine::MenuManager.items(menu_name).size >= count, "Menu has less than #{count} items"
+ end
+
+ def assert_menu_contains_item_named(menu_name, item_name)
+ assert Redmine::MenuManager.items(menu_name).collect(&:name).include?(item_name.to_sym), "Menu did not have an item named #{item_name}"
+ end
+
+ # Helpers
+ def get_menu_item(menu_name, item_name)
+ Redmine::MenuManager.items(menu_name).find {|item| item.name == item_name.to_sym}
+ end
+end
+
+class RedmineTest < Test::Unit::TestCase
+ include RedmineMenuTestHelper
+
+ def test_top_menu
+ assert_number_of_items_in_menu :top_menu, 5
+ assert_menu_contains_item_named :top_menu, :home
+ assert_menu_contains_item_named :top_menu, :my_page
+ assert_menu_contains_item_named :top_menu, :projects
+ assert_menu_contains_item_named :top_menu, :administration
+ assert_menu_contains_item_named :top_menu, :help
+ end
+
+ def test_account_menu
+ assert_number_of_items_in_menu :account_menu, 4
+ assert_menu_contains_item_named :account_menu, :login
+ assert_menu_contains_item_named :account_menu, :register
+ assert_menu_contains_item_named :account_menu, :my_account
+ assert_menu_contains_item_named :account_menu, :logout
+ end
+
+ def test_application_menu
+ assert_number_of_items_in_menu :application_menu, 0
+ end
+
+ def test_admin_menu
+ assert_number_of_items_in_menu :admin_menu, 0
+ end
+
+ def test_project_menu
+ assert_number_of_items_in_menu :project_menu, 12
+ assert_menu_contains_item_named :project_menu, :overview
+ assert_menu_contains_item_named :project_menu, :activity
+ assert_menu_contains_item_named :project_menu, :roadmap
+ assert_menu_contains_item_named :project_menu, :issues
+ assert_menu_contains_item_named :project_menu, :new_issue
+ assert_menu_contains_item_named :project_menu, :news
+ assert_menu_contains_item_named :project_menu, :documents
+ assert_menu_contains_item_named :project_menu, :wiki
+ assert_menu_contains_item_named :project_menu, :boards
+ assert_menu_contains_item_named :project_menu, :files
+ assert_menu_contains_item_named :project_menu, :repository
+ assert_menu_contains_item_named :project_menu, :settings
+ end
+
+ def test_new_issue_should_have_root_as_a_parent
+ new_issue = get_menu_item(:project_menu, :new_issue)
+ assert_equal :root, new_issue.parent.name
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class MailHandlerTest < ActiveSupport::TestCase
+ fixtures :users, :projects,
+ :enabled_modules,
+ :roles,
+ :members,
+ :member_roles,
+ :issues,
+ :issue_statuses,
+ :workflows,
+ :trackers,
+ :projects_trackers,
+ :enumerations,
+ :issue_categories,
+ :custom_fields,
+ :custom_fields_trackers,
+ :boards,
+ :messages
+
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
+
+ def setup
+ ActionMailer::Base.deliveries.clear
+ end
+
+ def test_add_issue
+ # 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 '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 IssueStatus.find_by_name('Resolved'), issue.status
+ assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
+ # keywords should be removed from the email body
+ assert !issue.description.match(/^Project:/i)
+ assert !issue.description.match(/^Status:/i)
+ 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_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 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
+ assert !issue.description.match(/^searchable field:/i)
+ 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.watchers.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_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 = email.body.match(/\* Login: (.*)$/)[1]
+ password = email.body.match(/\* Password: (.*)$/)[1]
+ assert_equal issue.author, User.try_to_login(login, password)
+ end
+ 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_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_add_issue_should_send_email_notification
+ 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_add_issue_note
+ 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
+ end
+
+ def test_add_issue_note_with_status_change
+ # 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 IssueStatus.find_by_name("Resolved"), issue.status
+ end
+
+ def test_add_issue_note_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_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
+
+ private
+
+ def submit_email(filename, options={})
+ raw = IO.read(File.join(FIXTURES_PATH, filename))
+ MailHandler.receive(raw, options)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class MailerTest < ActiveSupport::TestCase
+ include Redmine::I18n
+ include ActionController::Assertions::SelectorAssertions
+ fixtures :projects, :issues, :users, :members, :member_roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
+
+ def test_generated_links_in_emails
+ ActionMailer::Base.deliveries.clear
+ Setting.host_name = 'mydomain.foo'
+ Setting.protocol = 'https'
+
+ journal = Journal.find(2)
+ assert Mailer.deliver_issue_edit(journal)
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+
+ assert_select_email do
+ # link to the main ticket
+ assert_select "a[href=?]", "https://mydomain.foo/issues/1", :text => "Bug #1: Can't print recipes"
+ # link to a referenced ticket
+ assert_select "a[href=?][title=?]", "https://mydomain.foo/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
+ # link to a changeset
+ assert_select "a[href=?][title=?]", "https://mydomain.foo/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 & #3", :text => "r2"
+ end
+ end
+
+ def test_generated_links_with_prefix
+ relative_url_root = Redmine::Utils.relative_url_root
+ ActionMailer::Base.deliveries.clear
+ Setting.host_name = 'mydomain.foo/rdm'
+ Setting.protocol = 'http'
+ Redmine::Utils.relative_url_root = '/rdm'
+
+ journal = Journal.find(2)
+ assert Mailer.deliver_issue_edit(journal)
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+
+ assert_select_email do
+ # link to the main ticket
+ assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
+ # link to a referenced ticket
+ assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
+ # link to a changeset
+ assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 & #3", :text => "r2"
+ end
+ ensure
+ # restore it
+ Redmine::Utils.relative_url_root = relative_url_root
+ end
+
+ def test_generated_links_with_prefix_and_no_relative_url_root
+ relative_url_root = Redmine::Utils.relative_url_root
+ ActionMailer::Base.deliveries.clear
+ Setting.host_name = 'mydomain.foo/rdm'
+ Setting.protocol = 'http'
+ Redmine::Utils.relative_url_root = nil
+
+ journal = Journal.find(2)
+ assert Mailer.deliver_issue_edit(journal)
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+
+ assert_select_email do
+ # link to the main ticket
+ assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
+ # link to a referenced ticket
+ assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
+ # link to a changeset
+ assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 & #3", :text => "r2"
+ end
+ ensure
+ # restore it
+ Redmine::Utils.relative_url_root = relative_url_root
+ end
+
+ def test_email_headers
+ ActionMailer::Base.deliveries.clear
+ issue = Issue.find(1)
+ Mailer.deliver_issue_add(issue)
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal 'bulk', mail.header_string('Precedence')
+ assert_equal 'auto-generated', mail.header_string('Auto-Submitted')
+ end
+
+ def test_plain_text_mail
+ Setting.plain_text_mail = 1
+ journal = Journal.find(2)
+ Mailer.deliver_issue_edit(journal)
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal "text/plain", mail.content_type
+ assert_equal 0, mail.parts.size
+ assert !mail.encoded.include?('href')
+ end
+
+ def test_html_mail
+ Setting.plain_text_mail = 0
+ journal = Journal.find(2)
+ Mailer.deliver_issue_edit(journal)
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal 2, mail.parts.size
+ assert mail.encoded.include?('href')
+ end
+
+ def test_issue_add_message_id
+ ActionMailer::Base.deliveries.clear
+ issue = Issue.find(1)
+ Mailer.deliver_issue_add(issue)
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal Mailer.message_id_for(issue), mail.message_id
+ assert_nil mail.references
+ end
+
+ def test_issue_edit_message_id
+ ActionMailer::Base.deliveries.clear
+ journal = Journal.find(1)
+ Mailer.deliver_issue_edit(journal)
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal Mailer.message_id_for(journal), mail.message_id
+ assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s
+ end
+
+ def test_message_posted_message_id
+ ActionMailer::Base.deliveries.clear
+ message = Message.find(1)
+ Mailer.deliver_message_posted(message, message.author.mail)
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal Mailer.message_id_for(message), mail.message_id
+ assert_nil mail.references
+ end
+
+ def test_reply_posted_message_id
+ ActionMailer::Base.deliveries.clear
+ message = Message.find(3)
+ Mailer.deliver_message_posted(message, message.author.mail)
+ mail = ActionMailer::Base.deliveries.last
+ assert_not_nil mail
+ assert_equal Mailer.message_id_for(message), mail.message_id
+ assert_equal Mailer.message_id_for(message.parent), mail.references.first.to_s
+ end
+
+ # test mailer methods for each language
+ def test_issue_add
+ issue = Issue.find(1)
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_issue_add(issue)
+ end
+ end
+
+ def test_issue_edit
+ journal = Journal.find(1)
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_issue_edit(journal)
+ end
+ end
+
+ def test_document_added
+ document = Document.find(1)
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_document_added(document)
+ end
+ end
+
+ def test_attachments_added
+ attachements = [ Attachment.find_by_container_type('Document') ]
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_attachments_added(attachements)
+ end
+ end
+
+ def test_news_added
+ news = News.find(:first)
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_news_added(news)
+ end
+ end
+
+ def test_message_posted
+ message = Message.find(:first)
+ recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
+ recipients = recipients.compact.uniq
+ valid_languages.each do |lang|
+ Setting.default_language = lang.to_s
+ assert Mailer.deliver_message_posted(message, recipients)
+ end
+ end
+
+ def test_account_information
+ user = User.find(:first)
+ valid_languages.each do |lang|
+ user.update_attribute :language, lang.to_s
+ user.reload
+ assert Mailer.deliver_account_information(user, 'pAsswORd')
+ end
+ end
+
+ def test_lost_password
+ token = Token.find(2)
+ valid_languages.each do |lang|
+ token.user.update_attribute :language, lang.to_s
+ token.reload
+ assert Mailer.deliver_lost_password(token)
+ end
+ end
+
+ def test_register
+ token = Token.find(1)
+ Setting.host_name = 'redmine.foo'
+ Setting.protocol = 'https'
+
+ valid_languages.each do |lang|
+ token.user.update_attribute :language, lang.to_s
+ token.reload
+ ActionMailer::Base.deliveries.clear
+ assert Mailer.deliver_register(token)
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.body.include?("https://redmine.foo/account/activate?token=#{token.value}")
+ end
+ end
+
+ def test_reminders
+ ActionMailer::Base.deliveries.clear
+ Mailer.reminders(:days => 42)
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.bcc.include?('dlopper@somenet.foo')
+ assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class MemberTest < ActiveSupport::TestCase
+ fixtures :users, :projects, :roles, :members, :member_roles
+
+ def setup
+ @jsmith = Member.find(1)
+ end
+
+ def test_create
+ member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
+ assert member.save
+ member.reload
+
+ assert_equal 2, member.roles.size
+ assert_equal Role.find(1), member.roles.sort.first
+ end
+
+ def test_update
+ assert_equal "eCookbook", @jsmith.project.name
+ assert_equal "Manager", @jsmith.roles.first.name
+ assert_equal "jsmith", @jsmith.user.login
+
+ @jsmith.mail_notification = !@jsmith.mail_notification
+ assert @jsmith.save
+ end
+
+ def test_update_roles
+ assert_equal 1, @jsmith.roles.size
+ @jsmith.role_ids = [1, 2]
+ assert @jsmith.save
+ assert_equal 2, @jsmith.reload.roles.size
+ end
+
+ def test_validate
+ member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
+ # same use can't have more than one membership for a project
+ assert !member.save
+
+ member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [])
+ # must have one role at least
+ assert !member.save
+ end
+
+ def test_destroy
+ assert_difference 'Member.count', -1 do
+ assert_difference 'MemberRole.count', -1 do
+ @jsmith.destroy
+ end
+ end
+
+ assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+begin
+ require 'mocha'
+
+ class MercurialAdapterTest < ActiveSupport::TestCase
+
+ TEMPLATES_DIR = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATES_DIR
+ TEMPLATE_NAME = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_NAME
+ TEMPLATE_EXTENSION = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_EXTENSION
+
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
+
+ def test_hgversion
+ to_test = { "0.9.5" => [0,9,5],
+ "1.0" => [1,0],
+ "1e4ddc9ac9f7+20080325" => nil,
+ "1.0.1+20080525" => [1,0,1],
+ "1916e629a29d" => nil}
+
+ to_test.each do |s, v|
+ test_hgversion_for(s, v)
+ end
+ end
+
+ def test_template_path
+ to_test = { [0,9,5] => "0.9.5",
+ [1,0] => "1.0",
+ [] => "1.0",
+ [1,0,1] => "1.0"}
+
+ to_test.each do |v, template|
+ test_template_path_for(v, template)
+ end
+ end
+
+ private
+
+ def test_hgversion_for(hgversion, version)
+ Redmine::Scm::Adapters::MercurialAdapter.expects(:hgversion_from_command_line).returns(hgversion)
+ adapter = Redmine::Scm::Adapters::MercurialAdapter
+ assert_equal version, adapter.hgversion
+ end
+
+ def test_template_path_for(version, template)
+ adapter = Redmine::Scm::Adapters::MercurialAdapter
+ assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{template}.#{TEMPLATE_EXTENSION}", adapter.template_path_for(version)
+ assert File.exist?(adapter.template_path_for(version))
+ end
+ end
+
+rescue LoadError
+ def test_fake; assert(false, "Requires mocha to run those tests") end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class MessageTest < ActiveSupport::TestCase
+ fixtures :projects, :roles, :members, :member_roles, :boards, :messages, :users, :watchers
+
+ def setup
+ @board = Board.find(1)
+ @user = User.find(1)
+ end
+
+ def test_create
+ topics_count = @board.topics_count
+ messages_count = @board.messages_count
+
+ message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
+ assert message.save
+ @board.reload
+ # topics count incremented
+ assert_equal topics_count+1, @board[:topics_count]
+ # messages count incremented
+ 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)
+ end
+
+ def test_reply
+ topics_count = @board.topics_count
+ messages_count = @board.messages_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)
+ assert reply.save
+ @board.reload
+ # same topics count
+ assert_equal topics_count, @board[:topics_count]
+ # messages count incremented
+ assert_equal messages_count+1, @board[:messages_count]
+ assert_equal reply, @board.last_message
+ @message.reload
+ # replies count incremented
+ 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)
+ end
+
+ def test_moving_message_should_update_counters
+ @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
+ # 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)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def test_destroy_topic
+ message = Message.find(1)
+ board = message.board
+ topics_count, messages_count = board.topics_count, board.messages_count
+
+ assert_difference('Watcher.count', -1) do
+ assert message.destroy
+ end
+ board.reload
+
+ # Replies deleted
+ assert Message.find_all_by_parent_id(1).empty?
+ # Checks counters
+ assert_equal topics_count - 1, board.topics_count
+ assert_equal messages_count - 3, board.messages_count
+ # Watchers removed
+ end
+
+ def test_destroy_reply
+ message = Message.find(5)
+ board = message.board
+ topics_count, messages_count = board.topics_count, board.messages_count
+ assert message.destroy
+ board.reload
+
+ # Checks counters
+ assert_equal topics_count, board.topics_count
+ assert_equal messages_count - 1, board.messages_count
+ end
+
+ def test_editable_by
+ message = Message.find(6)
+ author = message.author
+ assert message.editable_by?(author)
+
+ author.roles_for_project(message.project).first.remove_permission!(:edit_own_messages)
+ assert !message.reload.editable_by?(author.reload)
+ end
+
+ def test_destroyable_by
+ message = Message.find(6)
+ author = message.author
+ assert message.destroyable_by?(author)
+
+ author.roles_for_project(message.project).first.remove_permission!(:delete_own_messages)
+ assert !message.reload.destroyable_by?(author.reload)
+ end
+
+ def test_set_sticky
+ message = Message.new
+ assert_equal 0, message.sticky
+ message.sticky = nil
+ assert_equal 0, message.sticky
+ message.sticky = false
+ assert_equal 0, message.sticky
+ message.sticky = true
+ assert_equal 1, message.sticky
+ message.sticky = '0'
+ assert_equal 0, message.sticky
+ message.sticky = '1'
+ assert_equal 1, message.sticky
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class NewsTest < ActiveSupport::TestCase
+ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news
+
+ def valid_news
+ { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) }
+ end
+
+
+ def setup
+ end
+
+ def test_create_should_send_email_notification
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events << 'news_added'
+ news = Project.find(:first).news.new(valid_news)
+
+ assert news.save
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_should_include_news_for_projects_with_news_enabled
+ project = projects(:projects_001)
+ assert project.enabled_modules.any?{ |em| em.name == 'news' }
+
+ # News.latest should return news from projects_001
+ assert News.latest.any? { |news| news.project == project }
+ end
+
+ def test_should_not_include_news_for_projects_with_news_disabled
+ # The projects_002 (OnlineStore) doesn't have the news module enabled, use that project for this test
+ project = projects(:projects_002)
+ assert ! project.enabled_modules.any?{ |em| em.name == 'news' }
+
+ # Add a piece of news to the project
+ news = project.news.create(valid_news)
+
+ # News.latest should not return that new piece of news
+ assert News.latest.include?(news) == false
+ end
+
+ def test_should_only_include_news_from_projects_visibly_to_the_user
+ # users_001 has no memberships so can only get news from public project
+ assert News.latest(users(:users_001)).all? { |news| news.project.is_public? }
+ end
+
+ def test_should_limit_the_amount_of_returned_news
+ # Make sure we have a bunch of news stories
+ 10.times { projects(:projects_001).news.create(valid_news) }
+ assert_equal 2, News.latest(users(:users_002), 2).size
+ assert_equal 6, News.latest(users(:users_002), 6).size
+ end
+
+ def test_should_return_5_news_stories_by_default
+ # Make sure we have a bunch of news stories
+ 10.times { projects(:projects_001).news.create(valid_news) }
+ assert_equal 5, News.latest(users(:users_004)).size
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class ProjectTest < ActiveSupport::TestCase
+ fixtures :projects, :enabled_modules,
+ :issues, :issue_statuses, :journals, :journal_details,
+ :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
+ :queries
+
+ def setup
+ @ecookbook = Project.find(1)
+ @ecookbook_sub1 = Project.find(3)
+ User.current = nil
+ end
+
+ should_validate_presence_of :name
+ should_validate_presence_of :identifier
+
+ should_validate_uniqueness_of :name
+ should_validate_uniqueness_of :identifier
+
+ context "associations" do
+ should_have_many :members
+ should_have_many :users, :through => :members
+ should_have_many :member_principals
+ should_have_many :principals, :through => :member_principals
+ should_have_many :enabled_modules
+ should_have_many :issues
+ should_have_many :issue_changes, :through => :issues
+ should_have_many :versions
+ should_have_many :time_entries
+ should_have_many :queries
+ should_have_many :documents
+ should_have_many :news
+ should_have_many :issue_categories
+ should_have_many :boards
+ should_have_many :changesets, :through => :repository
+
+ should_have_one :repository
+ should_have_one :wiki
+
+ should_have_and_belong_to_many :trackers
+ should_have_and_belong_to_many :issue_custom_fields
+ end
+
+ def test_truth
+ assert_kind_of Project, @ecookbook
+ assert_equal "eCookbook", @ecookbook.name
+ end
+
+ def test_update
+ assert_equal "eCookbook", @ecookbook.name
+ @ecookbook.name = "eCook"
+ assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
+ @ecookbook.reload
+ assert_equal "eCook", @ecookbook.name
+ end
+
+ def test_validate_identifier
+ to_test = {"abc" => true,
+ "ab12" => true,
+ "ab-12" => true,
+ "12" => false,
+ "new" => false}
+
+ to_test.each do |identifier, valid|
+ p = Project.new
+ p.identifier = identifier
+ p.valid?
+ assert_equal valid, p.errors.on('identifier').nil?
+ end
+ end
+
+ def test_members_should_be_active_users
+ Project.all.each do |project|
+ assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
+ end
+ end
+
+ def test_users_should_be_active_users
+ Project.all.each do |project|
+ assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
+ end
+ end
+
+ def test_archive
+ user = @ecookbook.members.first.user
+ @ecookbook.archive
+ @ecookbook.reload
+
+ assert !@ecookbook.active?
+ assert !user.projects.include?(@ecookbook)
+ # Subproject are also archived
+ assert !@ecookbook.children.empty?
+ assert @ecookbook.descendants.active.empty?
+ end
+
+ def test_unarchive
+ user = @ecookbook.members.first.user
+ @ecookbook.archive
+ # A subproject of an archived project can not be unarchived
+ assert !@ecookbook_sub1.unarchive
+
+ # Unarchive project
+ assert @ecookbook.unarchive
+ @ecookbook.reload
+ assert @ecookbook.active?
+ assert user.projects.include?(@ecookbook)
+ # Subproject can now be unarchived
+ @ecookbook_sub1.reload
+ assert @ecookbook_sub1.unarchive
+ end
+
+ def test_destroy
+ # 2 active members
+ assert_equal 2, @ecookbook.members.size
+ # and 1 is locked
+ assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
+ # some boards
+ assert @ecookbook.boards.any?
+
+ @ecookbook.destroy
+ # make sure that the project non longer exists
+ assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
+ # make sure related data was removed
+ assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
+ assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
+ end
+
+ def test_move_an_orphan_project_to_a_root_project
+ sub = Project.find(2)
+ sub.set_parent! @ecookbook
+ assert_equal @ecookbook.id, sub.parent.id
+ @ecookbook.reload
+ assert_equal 4, @ecookbook.children.size
+ end
+
+ def test_move_an_orphan_project_to_a_subproject
+ sub = Project.find(2)
+ assert sub.set_parent!(@ecookbook_sub1)
+ end
+
+ def test_move_a_root_project_to_a_project
+ sub = @ecookbook
+ assert sub.set_parent!(Project.find(2))
+ end
+
+ def test_should_not_move_a_project_to_its_children
+ sub = @ecookbook
+ assert !(sub.set_parent!(Project.find(3)))
+ end
+
+ def test_set_parent_should_add_roots_in_alphabetical_order
+ ProjectCustomField.delete_all
+ Project.delete_all
+ Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
+ Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
+ Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
+ Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
+
+ assert_equal 4, Project.count
+ assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
+ end
+
+ def test_set_parent_should_add_children_in_alphabetical_order
+ ProjectCustomField.delete_all
+ parent = Project.create!(:name => 'Parent', :identifier => 'parent')
+ Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
+ Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
+ Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
+ Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
+
+ parent.reload
+ assert_equal 4, parent.children.size
+ assert_equal parent.children.sort_by(&:name), parent.children
+ end
+
+ def test_rebuild_should_sort_children_alphabetically
+ ProjectCustomField.delete_all
+ parent = Project.create!(:name => 'Parent', :identifier => 'parent')
+ Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
+ Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
+ Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
+ Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
+
+ Project.update_all("lft = NULL, rgt = NULL")
+ Project.rebuild!
+
+ parent.reload
+ assert_equal 4, parent.children.size
+ assert_equal parent.children.sort_by(&:name), parent.children
+ end
+
+ def test_parent
+ p = Project.find(6).parent
+ assert p.is_a?(Project)
+ assert_equal 5, p.id
+ end
+
+ def test_ancestors
+ a = Project.find(6).ancestors
+ assert a.first.is_a?(Project)
+ assert_equal [1, 5], a.collect(&:id)
+ end
+
+ def test_root
+ r = Project.find(6).root
+ assert r.is_a?(Project)
+ assert_equal 1, r.id
+ end
+
+ def test_children
+ c = Project.find(1).children
+ assert c.first.is_a?(Project)
+ assert_equal [5, 3, 4], c.collect(&:id)
+ end
+
+ def test_descendants
+ d = Project.find(1).descendants
+ assert d.first.is_a?(Project)
+ assert_equal [5, 6, 3, 4], d.collect(&:id)
+ end
+
+ def test_allowed_parents_should_be_empty_for_non_member_user
+ Role.non_member.add_permission!(:add_project)
+ user = User.find(9)
+ assert user.memberships.empty?
+ User.current = user
+ assert Project.new.allowed_parents.empty?
+ end
+
+ def test_users_by_role
+ users_by_role = Project.find(1).users_by_role
+ assert_kind_of Hash, users_by_role
+ role = Role.find(1)
+ assert_kind_of Array, users_by_role[role]
+ assert users_by_role[role].include?(User.find(2))
+ end
+
+ def test_rolled_up_trackers
+ parent = Project.find(1)
+ parent.trackers = Tracker.find([1,2])
+ child = parent.children.find(3)
+
+ assert_equal [1, 2], parent.tracker_ids
+ assert_equal [2, 3], child.trackers.collect(&:id)
+
+ assert_kind_of Tracker, parent.rolled_up_trackers.first
+ assert_equal Tracker.find(1), parent.rolled_up_trackers.first
+
+ assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
+ assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
+ end
+
+ def test_rolled_up_trackers_should_ignore_archived_subprojects
+ parent = Project.find(1)
+ parent.trackers = Tracker.find([1,2])
+ child = parent.children.find(3)
+ child.trackers = Tracker.find([1,3])
+ parent.children.each(&:archive)
+
+ assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
+ end
+
+ def test_next_identifier
+ ProjectCustomField.delete_all
+ Project.create!(:name => 'last', :identifier => 'p2008040')
+ assert_equal 'p2008041', Project.next_identifier
+ end
+
+ def test_next_identifier_first_project
+ Project.delete_all
+ assert_nil Project.next_identifier
+ end
+
+
+ def test_enabled_module_names_should_not_recreate_enabled_modules
+ project = Project.find(1)
+ # Remove one module
+ modules = project.enabled_modules.slice(0..-2)
+ assert modules.any?
+ assert_difference 'EnabledModule.count', -1 do
+ project.enabled_module_names = modules.collect(&:name)
+ end
+ project.reload
+ # Ids should be preserved
+ assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
+ end
+
+ def test_copy_from_existing_project
+ source_project = Project.find(1)
+ copied_project = Project.copy_from(1)
+
+ assert copied_project
+ # Cleared attributes
+ assert copied_project.id.blank?
+ assert copied_project.name.blank?
+ assert copied_project.identifier.blank?
+
+ # Duplicated attributes
+ assert_equal source_project.description, copied_project.description
+ assert_equal source_project.enabled_modules, copied_project.enabled_modules
+ assert_equal source_project.trackers, copied_project.trackers
+
+ # Default attributes
+ assert_equal 1, copied_project.status
+ end
+
+ def test_activities_should_use_the_system_activities
+ project = Project.find(1)
+ assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
+ end
+
+
+ def test_activities_should_use_the_project_specific_activities
+ project = Project.find(1)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
+ assert overridden_activity.save!
+
+ assert project.activities.include?(overridden_activity), "Project specific Activity not found"
+ end
+
+ def test_activities_should_not_include_the_inactive_project_specific_activities
+ project = Project.find(1)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
+ assert overridden_activity.save!
+
+ assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
+ end
+
+ def test_activities_should_not_include_project_specific_activities_from_other_projects
+ project = Project.find(1)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
+ assert overridden_activity.save!
+
+ assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
+ end
+
+ def test_activities_should_handle_nils
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
+ TimeEntryActivity.delete_all
+
+ # No activities
+ project = Project.find(1)
+ assert project.activities.empty?
+
+ # No system, one overridden
+ assert overridden_activity.save!
+ project.reload
+ assert_equal [overridden_activity], project.activities
+ end
+
+ def test_activities_should_override_system_activities_with_project_activities
+ project = Project.find(1)
+ parent_activity = TimeEntryActivity.find(:first)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
+ assert overridden_activity.save!
+
+ assert project.activities.include?(overridden_activity), "Project specific Activity not found"
+ assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
+ end
+
+ def test_activities_should_include_inactive_activities_if_specified
+ project = Project.find(1)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
+ assert overridden_activity.save!
+
+ assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
+ end
+
+ def test_close_completed_versions
+ Version.update_all("status = 'open'")
+ project = Project.find(1)
+ assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
+ assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
+ project.close_completed_versions
+ project.reload
+ assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
+ assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
+ end
+
+ 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_id => 5,
+ :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 "change the new issues to use the copied version" do
+ assigned_version = Version.generate!(:name => "Assigned Issues")
+ @source_project.versions << assigned_version
+ assert_equal 1, @source_project.versions.size
+ @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
+ :subject => "change the new issues to use the copied version",
+ :tracker_id => 1,
+ :project_id => @source_project.id)
+
+ 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 "copy members" do
+ assert @project.valid?
+ assert @project.members.empty?
+ assert @project.copy(@source_project)
+
+ assert_equal @source_project.members.size, @project.members.size
+ @project.members.each do |member|
+ assert member
+ assert_equal @project, member.project
+ end
+ 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
+ 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" do
+ assert @project.copy(@source_project)
+
+ assert @project.wiki
+ assert_equal 1, @project.wiki.pages.length
+
+ @project.wiki.pages.each do |wiki_page|
+ assert wiki_page.content
+ assert !@source_project.wiki.pages.include?(wiki_page)
+ end
+ end
+
+ should "copy custom fields"
+
+ 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
+
+ should "copy issue relations"
+ should "link issue relations if cross project issue relations are valid"
+
+ end
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class QueryTest < ActiveSupport::TestCase
+ fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
+
+ def test_custom_fields_for_all_projects_should_be_available_in_global_queries
+ query = Query.new(:project => nil, :name => '_')
+ assert query.available_filters.has_key?('cf_1')
+ assert !query.available_filters.has_key?('cf_3')
+ end
+
+ def find_issues_with_query(query)
+ Issue.find :all,
+ :include => [ :assigned_to, :status, :tracker, :project, :priority ],
+ :conditions => query.statement
+ end
+
+ def test_query_with_multiple_custom_fields
+ query = Query.find(1)
+ assert query.valid?
+ assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
+ issues = find_issues_with_query(query)
+ assert_equal 1, issues.length
+ assert_equal Issue.find(3), issues.first
+ end
+
+ def test_operator_none
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('fixed_version_id', '!*', [''])
+ query.add_filter('cf_1', '!*', [''])
+ assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
+ assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_none_for_integer
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('estimated_hours', '!*', [''])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ assert issues.all? {|i| !i.estimated_hours}
+ end
+
+ def test_operator_all
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('fixed_version_id', '*', [''])
+ query.add_filter('cf_1', '*', [''])
+ assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
+ assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_greater_than
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('done_ratio', '>=', ['40'])
+ assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
+ 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.add_filter('due_date', '>t+', ['15'])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
+ end
+
+ def test_operator_in_less_than
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', '<t+', ['15'])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
+ end
+
+ def test_operator_less_than_ago
+ Issue.find(7).update_attribute(:due_date, (Date.today - 3))
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', '>t-', ['3'])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
+ end
+
+ def test_operator_more_than_ago
+ Issue.find(7).update_attribute(:due_date, (Date.today - 10))
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', '<t-', ['10'])
+ assert query.statement.include?("#{Issue.table_name}.due_date <=")
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
+ end
+
+ def test_operator_in
+ Issue.find(7).update_attribute(:due_date, (Date.today + 2))
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 't+', ['2'])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
+ end
+
+ def test_operator_ago
+ Issue.find(7).update_attribute(:due_date, (Date.today - 3))
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 't-', ['3'])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
+ end
+
+ def test_operator_today
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 't', [''])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ issues.each {|issue| assert_equal Date.today, issue.due_date}
+ end
+
+ def test_operator_this_week_on_date
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 'w', [''])
+ find_issues_with_query(query)
+ end
+
+ def test_operator_this_week_on_datetime
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('created_on', 'w', [''])
+ find_issues_with_query(query)
+ end
+
+ def test_operator_contains
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('subject', '~', ['uNable'])
+ assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
+ result = find_issues_with_query(query)
+ assert result.empty?
+ result.each {|issue| assert issue.subject.downcase.include?('unable') }
+ end
+
+ def test_operator_does_not_contains
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('subject', '!~', ['uNable'])
+ assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
+ find_issues_with_query(query)
+ end
+
+ def test_filter_watched_issues
+ User.current = User.find(1)
+ query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
+ result = find_issues_with_query(query)
+ assert_not_nil result
+ assert !result.empty?
+ assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
+ User.current = nil
+ end
+
+ def test_filter_unwatched_issues
+ User.current = User.find(1)
+ query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
+ result = find_issues_with_query(query)
+ assert_not_nil result
+ assert !result.empty?
+ assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
+ User.current = nil
+ end
+
+ def test_default_columns
+ q = Query.new
+ assert !q.columns.empty?
+ end
+
+ def test_set_column_names
+ q = Query.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)
+ end
+
+ def test_groupable_columns_should_include_custom_fields
+ q = Query.new
+ assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
+ end
+
+ def test_include_options
+ q = Query.new
+ q.column_names = %w(subject tracker)
+ assert_equal [:tracker], q.include_options
+
+ q.group_by = 'category'
+ assert_equal [:tracker, :category], q.include_options
+ end
+
+ def test_default_sort
+ q = Query.new
+ assert_equal [], q.sort_criteria
+ end
+
+ def test_set_sort_criteria_with_hash
+ q = Query.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.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.sort_criteria = [['priority', 'desc'], 'tracker']
+ assert q.save
+ q.reload
+ assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
+ end
+
+ def test_sort_by_string_custom_field_asc
+ q = Query.new
+ c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
+ assert c
+ assert c.sortable
+ issues = Issue.find :all,
+ :include => [ :assigned_to, :status, :tracker, :project, :priority ],
+ :conditions => q.statement,
+ :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
+ c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
+ assert c
+ assert c.sortable
+ issues = Issue.find :all,
+ :include => [ :assigned_to, :status, :tracker, :project, :priority ],
+ :conditions => q.statement,
+ :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
+ c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
+ assert c
+ assert c.sortable
+ issues = Issue.find :all,
+ :include => [ :assigned_to, :status, :tracker, :project, :priority ],
+ :conditions => q.statement,
+ :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_label_for
+ q = Query.new
+ assert_equal 'assigned_to', q.label_for('assigned_to_id')
+ end
+
+ def test_editable_by
+ admin = User.find(1)
+ manager = User.find(2)
+ developer = User.find(3)
+
+ # Public query on project 1
+ q = Query.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)
+ assert q.editable_by?(admin)
+ assert !q.editable_by?(manager)
+ assert q.editable_by?(developer)
+
+ # Private query for all projects
+ q = Query.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)
+ assert q.editable_by?(admin)
+ assert !q.editable_by?(manager)
+ assert !q.editable_by?(developer)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryBazaarTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
+ REPOSITORY_PATH.gsub!(/\/+/, '/')
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Bazaar.create(:project => @project, :url => "file:///#{REPOSITORY_PATH}")
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 4, @repository.changesets.count
+ assert_equal 9, @repository.changes.count
+ assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove changesets with revision > 5
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
+ @repository.reload
+ assert_equal 2, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 4, @repository.changesets.count
+ end
+
+ def test_entries
+ entries = @repository.entries
+ assert_equal 2, entries.size
+
+ assert_equal 'dir', entries[0].kind
+ assert_equal 'directory', entries[0].name
+
+ assert_equal 'file', entries[1].kind
+ assert_equal 'doc-mkdir.txt', entries[1].name
+ end
+
+ def test_entries_in_subdirectory
+ entries = @repository.entries('directory')
+ assert_equal 3, entries.size
+
+ assert_equal 'file', entries.last.kind
+ assert_equal 'edit.png', entries.last.name
+ end
+
+ def test_cat
+ cat = @repository.scm.cat('directory/document.txt')
+ assert cat =~ /Write the contents of a file as of a given revision to standard output/
+ end
+
+ def test_annotate
+ annotate = @repository.scm.annotate('doc-mkdir.txt')
+ assert_equal 17, annotate.lines.size
+ assert_equal 1, annotate.revisions[0].identifier
+ assert_equal 'jsmith@', annotate.revisions[0].author
+ assert_equal 'mkdir', annotate.lines[0]
+ end
+ else
+ puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+require 'pp'
+class RepositoryCvsTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+ # CVS module
+ MODULE_NAME = 'test'
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Cvs.create(:project => @project,
+ :root_url => REPOSITORY_PATH,
+ :url => MODULE_NAME)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 5, @repository.changesets.count
+ assert_equal 14, @repository.changes.count
+ assert_not_nil @repository.changesets.find_by_comments('Two files changed')
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove the 3 latest changesets
+ @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
+ @repository.reload
+ assert_equal 2, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 5, @repository.changesets.count
+ end
+
+ def test_deleted_files_should_not_be_listed
+ entries = @repository.entries('sources')
+ assert entries.detect {|e| e.name == 'watchers_controller.rb'}
+ assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
+ end
+ else
+ puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryDarcsTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Darcs.create(:project => @project, :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 6, @repository.changesets.count
+ assert_equal 13, @repository.changes.count
+ assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove changesets with revision > 3
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3}
+ @repository.reload
+ assert_equal 3, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 6, @repository.changesets.count
+ end
+
+ def test_deleted_files_should_not_be_listed
+ entries = @repository.entries('sources')
+ assert entries.detect {|e| e.name == 'watchers_controller.rb'}
+ assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
+ end
+
+ def test_cat
+ @repository.fetch_changesets
+ cat = @repository.cat("sources/welcome_controller.rb", 2)
+ assert_not_nil cat
+ assert cat.include?('class WelcomeController < ApplicationController')
+ end
+ else
+ puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryFilesystemTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
+
+ def setup
+ @project = Project.find(1)
+ Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem')
+ assert @repository = Repository::Filesystem.create(:project => @project, :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 0, @repository.changesets.count
+ assert_equal 0, @repository.changes.count
+ end
+
+ def test_entries
+ assert_equal 2, @repository.entries("", 2).size
+ assert_equal 2, @repository.entries("dir", 3).size
+ end
+
+ def test_cat
+ assert_equal "TEST CAT\n", @repository.scm.cat("test")
+ end
+
+ else
+ puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryGitTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Git.create(:project => @project, :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 12, @repository.changesets.count
+ assert_equal 20, @repository.changes.count
+
+ commit = @repository.changesets.find(:first, :order => 'committed_on ASC')
+ assert_equal "Initial import.\nThe repository contains 3 files.", commit.comments
+ assert_equal "jsmith <jsmith@foo.bar>", commit.committer
+ assert_equal User.find_by_login('jsmith'), commit.user
+ # TODO: add a commit with commit time <> author time to the test repository
+ assert_equal "2007-12-14 09:22:52".to_time, commit.committed_on
+ assert_equal "2007-12-14".to_date, commit.commit_date
+ assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", commit.revision
+ assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", commit.scmid
+ assert_equal 3, commit.changes.count
+ change = commit.changes.sort_by(&:path).first
+ assert_equal "README", change.path
+ assert_equal "A", change.action
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove the 3 latest changesets
+ @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
+ @repository.reload
+ assert_equal 9, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 12, @repository.changesets.count
+ end
+ else
+ puts "Git test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryMercurialTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 6, @repository.changesets.count
+ assert_equal 11, @repository.changes.count
+ assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove changesets with revision > 2
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
+ @repository.reload
+ assert_equal 3, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 6, @repository.changesets.count
+ end
+
+ def test_entries
+ assert_equal 2, @repository.entries("sources", 2).size
+ assert_equal 1, @repository.entries("sources", 3).size
+ end
+
+ def test_locate_on_outdated_repository
+ # Change the working dir state
+ %x{hg -R #{REPOSITORY_PATH} up -r 0}
+ assert_equal 1, @repository.entries("images", 0).size
+ assert_equal 2, @repository.entries("images").size
+ assert_equal 2, @repository.entries("images", 2).size
+ end
+
+
+ def test_cat
+ assert @repository.scm.cat("sources/welcome_controller.rb", 2)
+ assert_nil @repository.scm.cat("sources/welcome_controller.rb")
+ end
+
+ else
+ puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositorySubversionTest < ActiveSupport::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path for svn
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
+
+ def setup
+ @project = Project.find(1)
+ assert @repository = Repository::Subversion.create(:project => @project, :url => "file:///#{REPOSITORY_PATH}")
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets_from_scratch
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 10, @repository.changesets.count
+ assert_equal 18, @repository.changes.count
+ assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
+ end
+
+ def test_fetch_changesets_incremental
+ @repository.fetch_changesets
+ # Remove changesets with revision > 5
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
+ @repository.reload
+ assert_equal 5, @repository.changesets.count
+
+ @repository.fetch_changesets
+ assert_equal 10, @repository.changesets.count
+ end
+
+ def test_latest_changesets_with_limit
+ @repository.fetch_changesets
+ changesets = @repository.latest_changesets('', nil, 2)
+ assert_equal 2, changesets.size
+ assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
+ end
+
+ def test_latest_changesets_with_path
+ @repository.fetch_changesets
+ changesets = @repository.latest_changesets('subversion_test/folder/helloworld.rb', nil)
+ assert_equal %w(6 3 2), changesets.collect(&:revision)
+ end
+ else
+ puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryTest < ActiveSupport::TestCase
+ fixtures :projects,
+ :trackers,
+ :projects_trackers,
+ :repositories,
+ :issues,
+ :issue_statuses,
+ :changesets,
+ :changes,
+ :users,
+ :enumerations
+
+ def setup
+ @repository = Project.find(1).repository
+ end
+
+ def test_create
+ repository = Repository::Subversion.new(:project => Project.find(3))
+ assert !repository.save
+
+ repository.url = "svn://localhost"
+ assert repository.save
+ repository.reload
+
+ project = Project.find(3)
+ assert_equal repository, project.repository
+ end
+
+ def test_destroy
+ changesets = Changeset.count(:all, :conditions => "repository_id = 10")
+ changes = Change.count(:all, :conditions => "repository_id = 10", :include => :changeset)
+ assert_difference 'Changeset.count', -changesets do
+ assert_difference 'Change.count', -changes do
+ Repository.find(10).destroy
+ end
+ end
+ end
+
+ def test_should_not_create_with_disabled_scm
+ # disable Subversion
+ Setting.enabled_scm = ['Darcs', 'Git']
+ repository = Repository::Subversion.new(:project => Project.find(3), :url => "svn://localhost")
+ assert !repository.save
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), repository.errors.on(:type)
+ # re-enable Subversion for following tests
+ Setting.delete_all
+ end
+
+ 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.default_language = 'en'
+ ActionMailer::Base.deliveries.clear
+
+ # make sure issue 1 is not already closed
+ fixed_issue = Issue.find(1)
+ assert !fixed_issue.status.is_closed?
+ old_status = fixed_issue.status
+
+ Repository.scan_changesets_for_issue_ids
+ assert_equal [101, 102], Issue.find(3).changeset_ids
+
+ # fixed issues
+ fixed_issue.reload
+ assert fixed_issue.status.is_closed?
+ assert_equal 90, fixed_issue.done_ratio
+ assert_equal [101], fixed_issue.changeset_ids
+
+ # issue change
+ journal = fixed_issue.journals.find(:first, :order => 'created_on desc')
+ assert_equal User.find_by_login('dlopper'), journal.user
+ assert_equal 'Applied in changeset r2.', journal.notes
+
+ # 2 email notifications
+ assert_equal 2, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries.first
+ assert_kind_of TMail::Mail, mail
+ assert mail.subject.starts_with?("[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
+ assert mail.body.include?("Status changed from #{old_status} to #{fixed_issue.status}")
+
+ # ignoring commits referencing an issue of another project
+ assert_equal [], Issue.find(4).changesets
+ end
+
+ def test_for_changeset_comments_strip
+ repository = Repository::Mercurial.create( :project => Project.find( 4 ), :url => '/foo/bar/baz' )
+ comment = <<-COMMENT
+ This is a loooooooooooooooooooooooooooong comment
+
+
+ COMMENT
+ changeset = Changeset.new(
+ :comments => comment, :commit_date => Time.now, :revision => 0, :scmid => 'f39b7922fb3c',
+ :committer => 'foo <foo@example.com>', :committed_on => Time.now, :repository => repository )
+ assert( changeset.save )
+ assert_not_equal( comment, changeset.comments )
+ assert_equal( 'This is a loooooooooooooooooooooooooooong comment', changeset.comments )
+ end
+
+ def test_for_urls_strip
+ repository = Repository::Cvs.create(:project => Project.find(4), :url => ' :pserver:login:password@host:/path/to/the/repository',
+ :root_url => 'foo ')
+ assert repository.save
+ repository.reload
+ assert_equal ':pserver:login:password@host:/path/to/the/repository', repository.url
+ assert_equal 'foo', repository.root_url
+ end
+
+ def test_manual_user_mapping
+ assert_no_difference "Changeset.count(:conditions => 'user_id <> 2')" do
+ c = Changeset.create!(:repository => @repository, :committer => 'foo', :committed_on => Time.now, :revision => 100, :comments => 'Committed by foo.')
+ assert_nil c.user
+ @repository.committer_ids = {'foo' => '2'}
+ assert_equal User.find(2), c.reload.user
+ # committer is now mapped
+ c = Changeset.create!(:repository => @repository, :committer => 'foo', :committed_on => Time.now, :revision => 101, :comments => 'Another commit by foo.')
+ assert_equal User.find(2), c.user
+ end
+ end
+
+ def test_auto_user_mapping_by_username
+ c = Changeset.create!(:repository => @repository, :committer => 'jsmith', :committed_on => Time.now, :revision => 100, :comments => 'Committed by john.')
+ assert_equal User.find(2), c.user
+ end
+
+ def test_auto_user_mapping_by_email
+ c = Changeset.create!(:repository => @repository, :committer => 'john <jsmith@somenet.foo>', :committed_on => Time.now, :revision => 100, :comments => 'Committed by john.')
+ assert_equal User.find(2), c.user
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class RoleTest < ActiveSupport::TestCase
+ fixtures :roles, :workflows
+
+ def test_copy_workflows
+ source = Role.find(1)
+ assert_equal 90, source.workflows.size
+
+ target = Role.new(:name => 'Target')
+ assert target.save
+ target.workflows.copy(source)
+ target.reload
+ assert_equal 90, target.workflows.size
+ 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
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class SearchTest < ActiveSupport::TestCase
+ fixtures :users,
+ :members,
+ :member_roles,
+ :projects,
+ :roles,
+ :enabled_modules,
+ :issues,
+ :trackers,
+ :journals,
+ :journal_details,
+ :repositories,
+ :changesets
+
+ def setup
+ @project = Project.find(1)
+ @issue_keyword = '%unable to print recipes%'
+ @issue = Issue.find(1)
+ @changeset_keyword = '%very first commit%'
+ @changeset = Changeset.find(100)
+ end
+
+ def test_search_by_anonymous
+ User.current = nil
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Removes the :view_changesets permission from Anonymous role
+ remove_permission Role.anonymous, :view_changesets
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert !r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_by_user
+ User.current = User.find_by_login('rhill')
+ assert User.current.memberships.empty?
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Removes the :view_changesets permission from Non member role
+ remove_permission Role.non_member, :view_changesets
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert !r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_by_allowed_member
+ User.current = User.find_by_login('jsmith')
+ assert User.current.projects.include?(@project)
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+ end
+
+ def test_search_by_unallowed_member
+ # Removes the :view_changesets permission from user's and non member role
+ remove_permission Role.find(1), :view_changesets
+ remove_permission Role.non_member, :view_changesets
+
+ User.current = User.find_by_login('jsmith')
+ assert User.current.projects.include?(@project)
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_issue_with_multiple_hits_in_journals
+ i = Issue.find(1)
+ assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'")
+
+ r = Issue.search('%notes%').first
+ assert_equal 1, r.size
+ assert_equal i, r.first
+ end
+
+ private
+
+ def remove_permission(role, permission)
+ role.permissions = role.permissions - [ permission ]
+ role.save
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class SettingTest < ActiveSupport::TestCase
+
+ def test_read_default
+ assert_equal "Redmine", Setting.app_title
+ assert Setting.self_registration?
+ assert !Setting.login_required?
+ end
+
+ def test_update
+ Setting.app_title = "My title"
+ assert_equal "My title", Setting.app_title
+ # make sure db has been updated (INSERT)
+ assert_equal "My title", Setting.find_by_name('app_title').value
+
+ Setting.app_title = "My other title"
+ assert_equal "My other title", Setting.app_title
+ # make sure db has been updated (UPDATE)
+ assert_equal "My other title", Setting.find_by_name('app_title').value
+ end
+
+ def test_serialized_setting
+ Setting.notified_events = ['issue_added', 'issue_updated', 'news_added']
+ assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.notified_events
+ assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.find_by_name('notified_events').value
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 'mkmf'
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SubversionAdapterTest < ActiveSupport::TestCase
+
+ if find_executable0('svn')
+ def test_client_version
+ v = Redmine::Scm::Adapters::SubversionAdapter.client_version
+ assert v.is_a?(Array)
+ end
+ else
+ puts "Subversion binary NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+# Test case that checks that the testing infrastructure is setup correctly.
+class TestingTest < ActiveSupport::TestCase
+ def test_working
+ assert true
+ end
+
+ test "Rails 'test' case syntax" do
+ assert true
+ end
+
+ test "Generating with object_daddy" do
+ assert_difference "IssueStatus.count" do
+ IssueStatus.generate!
+ end
+ end
+
+ should "work with shoulda" do
+ assert true
+ end
+
+ context "works with a context" do
+ should "work" do
+ assert true
+ end
+ end
+
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class TimeEntryActivityTest < ActiveSupport::TestCase
+ fixtures :enumerations, :time_entries
+
+ def test_should_be_an_enumeration
+ assert TimeEntryActivity.ancestors.include?(Enumeration)
+ end
+
+ def test_objects_count
+ assert_equal 3, TimeEntryActivity.find_by_name("Design").objects_count
+ assert_equal 1, TimeEntryActivity.find_by_name("Development").objects_count
+ end
+
+ def test_option_name
+ assert_equal :enumeration_activities, TimeEntryActivity.new.option_name
+ end
+
+ def test_create_with_custom_field
+ field = TimeEntryActivityCustomField.find_by_name('Billable')
+ e = TimeEntryActivity.new(:name => 'Custom Data')
+ e.custom_field_values = {field.id => "1"}
+ assert e.save
+
+ e.reload
+ assert_equal "1", e.custom_value_for(field).value
+ end
+
+ def test_create_without_required_custom_field_should_fail
+ field = TimeEntryActivityCustomField.find_by_name('Billable')
+ field.update_attribute(:is_required, true)
+
+ e = TimeEntryActivity.new(:name => 'Custom Data')
+ assert !e.save
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), e.errors.on(:custom_values)
+ end
+
+ def test_create_with_required_custom_field_should_succeed
+ field = TimeEntryActivityCustomField.find_by_name('Billable')
+ field.update_attribute(:is_required, true)
+
+ e = TimeEntryActivity.new(:name => 'Custom Data')
+ e.custom_field_values = {field.id => "1"}
+ assert e.save
+ end
+
+ def test_update_issue_with_required_custom_field_change
+ field = TimeEntryActivityCustomField.find_by_name('Billable')
+ field.update_attribute(:is_required, true)
+
+ e = TimeEntryActivity.find(10)
+ assert e.available_custom_fields.include?(field)
+ # No change to custom field, record can be saved
+ assert e.save
+ # Blanking custom field, save should fail
+ e.custom_field_values = {field.id => ""}
+ assert !e.save
+ assert e.errors.on(:custom_values)
+
+ # Update custom field to valid value, save should succeed
+ e.custom_field_values = {field.id => "0"}
+ assert e.save
+ e.reload
+ assert_equal "0", e.custom_value_for(field).value
+ end
+
+end
+
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class TimeEntryTest < ActiveSupport::TestCase
+ fixtures :issues, :projects, :users, :time_entries
+
+ def test_hours_format
+ assertions = { "2" => 2.0,
+ "21.1" => 21.1,
+ "2,1" => 2.1,
+ "1,5h" => 1.5,
+ "7:12" => 7.2,
+ "10h" => 10.0,
+ "10 h" => 10.0,
+ "45m" => 0.75,
+ "45 m" => 0.75,
+ "3h15" => 3.25,
+ "3h 15" => 3.25,
+ "3 h 15" => 3.25,
+ "3 h 15m" => 3.25,
+ "3 h 15 m" => 3.25,
+ "3 hours" => 3.0,
+ "12min" => 0.2,
+ }
+
+ assertions.each do |k, v|
+ t = TimeEntry.new(:hours => k)
+ assert_equal v, t.hours, "Converting #{k} failed:"
+ end
+ end
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TokenTest < ActiveSupport::TestCase
+ fixtures :tokens
+
+ def test_create
+ token = Token.new
+ token.save
+ assert_equal 40, token.value.length
+ assert !token.expired?
+ end
+
+ def test_create_should_remove_existing_tokens
+ user = User.find(1)
+ t1 = Token.create(:user => user, :action => 'autologin')
+ t2 = Token.create(:user => user, :action => 'autologin')
+ assert_not_equal t1.value, t2.value
+ assert !Token.exists?(t1.id)
+ assert Token.exists?(t2.id)
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class TrackerTest < ActiveSupport::TestCase
+ fixtures :trackers, :workflows
+
+ def test_copy_workflows
+ source = Tracker.find(1)
+ assert_equal 89, source.workflows.size
+
+ target = Tracker.new(:name => 'Target')
+ assert target.save
+ target.workflows.copy(source)
+ target.reload
+ assert_equal 89, target.workflows.size
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class UserPreferenceTest < ActiveSupport::TestCase
+ fixtures :users, :user_preferences
+
+ def test_create
+ user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
+ user.login = "newuser"
+ user.password, user.password_confirmation = "password", "password"
+ assert user.save
+
+ assert_kind_of UserPreference, user.pref
+ assert_kind_of Hash, user.pref.others
+ assert user.pref.save
+ end
+
+ def test_update
+ user = User.find(1)
+ assert_equal true, user.pref.hide_mail
+ user.pref['preftest'] = 'value'
+ assert user.pref.save
+
+ user.reload
+ assert_equal 'value', user.pref['preftest']
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTest < ActiveSupport::TestCase
+ fixtures :users, :members, :projects, :roles, :member_roles
+
+ def setup
+ @admin = User.find(1)
+ @jsmith = User.find(2)
+ @dlopper = User.find(3)
+ end
+
+ test 'object_daddy creation' do
+ User.generate_with_protected!(:firstname => 'Testing connection')
+ User.generate_with_protected!(:firstname => 'Testing connection')
+ assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
+ end
+
+ def test_truth
+ assert_kind_of User, @jsmith
+ end
+
+ def test_create
+ user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
+
+ user.login = "jsmith"
+ user.password, user.password_confirmation = "password", "password"
+ # login uniqueness
+ assert !user.save
+ assert_equal 1, user.errors.count
+
+ user.login = "newuser"
+ user.password, user.password_confirmation = "passwd", "password"
+ # password confirmation
+ assert !user.save
+ assert_equal 1, user.errors.count
+
+ user.password, user.password_confirmation = "password", "password"
+ assert user.save
+ end
+
+ def test_mail_uniqueness_should_not_be_case_sensitive
+ u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
+ u.login = 'newuser1'
+ u.password, u.password_confirmation = "password", "password"
+ assert u.save
+
+ u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
+ u.login = 'newuser2'
+ u.password, u.password_confirmation = "password", "password"
+ assert !u.save
+ assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
+ end
+
+ def test_update
+ assert_equal "admin", @admin.login
+ @admin.login = "john"
+ assert @admin.save, @admin.errors.full_messages.join("; ")
+ @admin.reload
+ assert_equal "john", @admin.login
+ end
+
+ def test_destroy
+ User.find(2).destroy
+ assert_nil User.find_by_id(2)
+ assert Member.find_all_by_user_id(2).empty?
+ end
+
+ def test_validate
+ @admin.login = ""
+ assert !@admin.save
+ assert_equal 1, @admin.errors.count
+ end
+
+ def test_password
+ user = User.try_to_login("admin", "admin")
+ assert_kind_of User, user
+ assert_equal "admin", user.login
+ user.password = "hello"
+ assert user.save
+
+ user = User.try_to_login("admin", "hello")
+ assert_kind_of User, user
+ assert_equal "admin", user.login
+ assert_equal User.hash_password("hello"), user.hashed_password
+ end
+
+ def test_name_format
+ assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
+ Setting.user_format = :firstname_lastname
+ assert_equal 'John Smith', @jsmith.reload.name
+ Setting.user_format = :username
+ assert_equal 'jsmith', @jsmith.reload.name
+ end
+
+ def test_lock
+ user = User.try_to_login("jsmith", "jsmith")
+ assert_equal @jsmith, user
+
+ @jsmith.status = User::STATUS_LOCKED
+ assert @jsmith.save
+
+ user = User.try_to_login("jsmith", "jsmith")
+ assert_equal nil, user
+ end
+
+ def test_create_anonymous
+ AnonymousUser.delete_all
+ anon = User.anonymous
+ assert !anon.new_record?
+ assert_kind_of AnonymousUser, anon
+ end
+
+ def test_rss_key
+ assert_nil @jsmith.rss_token
+ key = @jsmith.rss_key
+ assert_equal 40, key.length
+
+ @jsmith.reload
+ assert_equal key, @jsmith.rss_key
+ end
+
+ def test_roles_for_project
+ # user with a role
+ roles = @jsmith.roles_for_project(Project.find(1))
+ assert_kind_of Role, roles.first
+ assert_equal "Manager", roles.first.name
+
+ # user with no role
+ assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
+ end
+
+ def test_mail_notification_all
+ @jsmith.mail_notification = true
+ @jsmith.notified_project_ids = []
+ @jsmith.save
+ @jsmith.reload
+ assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
+ end
+
+ def test_mail_notification_selected
+ @jsmith.mail_notification = false
+ @jsmith.notified_project_ids = [1]
+ @jsmith.save
+ @jsmith.reload
+ assert Project.find(1).recipients.include?(@jsmith.mail)
+ end
+
+ def test_mail_notification_none
+ @jsmith.mail_notification = false
+ @jsmith.notified_project_ids = []
+ @jsmith.save
+ @jsmith.reload
+ assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
+ end
+
+ def test_comments_sorting_preference
+ assert !@jsmith.wants_comments_in_reverse_order?
+ @jsmith.pref.comments_sorting = 'asc'
+ assert !@jsmith.wants_comments_in_reverse_order?
+ @jsmith.pref.comments_sorting = 'desc'
+ assert @jsmith.wants_comments_in_reverse_order?
+ end
+
+ def test_find_by_mail_should_be_case_insensitive
+ u = User.find_by_mail('JSmith@somenet.foo')
+ assert_not_nil u
+ assert_equal 'jsmith@somenet.foo', u.mail
+ end
+
+ def test_random_password
+ u = User.new
+ u.random_password
+ assert !u.password.blank?
+ assert !u.password_confirmation.blank?
+ end
+
+ if Object.const_defined?(:OpenID)
+
+ def test_setting_identity_url
+ normalized_open_id_url = 'http://example.com/'
+ u = User.new( :identity_url => 'http://example.com/' )
+ assert_equal normalized_open_id_url, u.identity_url
+ end
+
+ def test_setting_identity_url_without_trailing_slash
+ normalized_open_id_url = 'http://example.com/'
+ u = User.new( :identity_url => 'http://example.com' )
+ assert_equal normalized_open_id_url, u.identity_url
+ end
+
+ def test_setting_identity_url_without_protocol
+ normalized_open_id_url = 'http://example.com/'
+ u = User.new( :identity_url => 'example.com' )
+ assert_equal normalized_open_id_url, u.identity_url
+ end
+
+ def test_setting_blank_identity_url
+ u = User.new( :identity_url => 'example.com' )
+ u.identity_url = ''
+ assert u.identity_url.blank?
+ end
+
+ def test_setting_invalid_identity_url
+ u = User.new( :identity_url => 'this is not an openid url' )
+ assert u.identity_url.blank?
+ end
+
+ else
+ puts "Skipping openid tests."
+ end
+
+end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class VersionTest < ActiveSupport::TestCase
+ fixtures :projects, :users, :issues, :issue_statuses, :trackers, :enumerations, :versions
+
+ def setup
+ end
+
+ def test_create
+ v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
+ assert v.save
+ assert_equal 'open', v.status
+ end
+
+ def test_invalid_effective_date_validation
+ v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01')
+ assert !v.save
+ assert_equal I18n.translate('activerecord.errors.messages.not_a_date'), v.errors.on(:effective_date)
+ end
+
+ 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
+ end
+
+ def test_progress_should_be_0_with_unbegun_assigned_issues
+ project = Project.find(1)
+ 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
+ end
+
+ def test_progress_should_be_100_with_closed_assigned_issues
+ project = Project.find(1)
+ status = IssueStatus.find(:first, :conditions => {:is_closed => true})
+ 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
+ end
+
+ def test_progress_should_consider_done_ratio_of_open_assigned_issues
+ project = Project.find(1)
+ v = Version.create!(:project => project, :name => 'Progress')
+ 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
+ end
+
+ def test_progress_should_consider_closed_issues_as_completed
+ project = Project.find(1)
+ 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
+ end
+
+ def test_progress_should_consider_estimated_hours_to_weigth_issues
+ project = Project.find(1)
+ v = Version.create!(:project => project, :name => 'Progress')
+ 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
+ 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, :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
+ end
+
+ private
+
+ def add_issue(version, attributes={})
+ Issue.create!({:project => version.project,
+ :fixed_version => version,
+ :subject => 'Test',
+ :author => User.find(:first),
+ :tracker => version.project.trackers.find(:first)}.merge(attributes))
+ end
+
+ def assert_progress_equal(expected_float, actual_float, message="")
+ assert_in_delta(expected_float, actual_float, 0.000001, message="")
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class WatcherTest < ActiveSupport::TestCase
+ fixtures :issues, :users
+
+ def setup
+ @user = User.find(1)
+ @issue = Issue.find(1)
+ 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_recipients
+ @issue.watchers.delete_all
+ @issue.reload
+
+ assert @issue.watcher_recipients.empty?
+ assert @issue.add_watcher(@user)
+
+ @user.mail_notification = true
+ @user.save
+ @issue.reload
+ assert @issue.watcher_recipients.include?(@user.mail)
+
+ @user.mail_notification = false
+ @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
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class WikiContentTest < ActiveSupport::TestCase
+ fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :users
+
+ def setup
+ @wiki = Wiki.find(1)
+ @page = @wiki.pages.first
+ end
+
+ def test_create
+ page = WikiPage.new(:wiki => @wiki, :title => "Page")
+ page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment")
+ assert page.save
+ page.reload
+
+ content = page.content
+ assert_kind_of WikiContent, content
+ assert_equal 1, content.version
+ assert_equal 1, content.versions.length
+ assert_equal "Content text", content.text
+ assert_equal "My comment", content.comments
+ assert_equal User.find(1), content.author
+ assert_equal content.text, content.versions.last.text
+ end
+
+ def test_create_should_send_email_notification
+ Setting.notified_events = ['wiki_content_added']
+ ActionMailer::Base.deliveries.clear
+ page = WikiPage.new(:wiki => @wiki, :title => "A new page")
+ page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment")
+ assert page.save
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_update
+ content = @page.content
+ version_count = content.version
+ content.text = "My new content"
+ assert content.save
+ content.reload
+ assert_equal version_count+1, content.version
+ assert_equal version_count+1, content.versions.length
+ end
+
+ def test_update_should_send_email_notification
+ Setting.notified_events = ['wiki_content_updated']
+ ActionMailer::Base.deliveries.clear
+ content = @page.content
+ content.text = "My new content"
+ assert content.save
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ end
+
+ def test_fetch_history
+ assert !@page.content.versions.empty?
+ @page.content.versions.each do |version|
+ assert_kind_of String, version.text
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class WikiPageTest < ActiveSupport::TestCase
+ fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
+
+ def setup
+ @wiki = Wiki.find(1)
+ @page = @wiki.pages.first
+ end
+
+ def test_create
+ page = WikiPage.new(:wiki => @wiki)
+ assert !page.save
+ assert_equal 1, page.errors.count
+
+ page.title = "Page"
+ assert page.save
+ page.reload
+
+ @wiki.reload
+ assert @wiki.pages.include?(page)
+ end
+
+ def test_find_or_new_page
+ page = @wiki.find_or_new_page("CookBook documentation")
+ assert_kind_of WikiPage, page
+ assert !page.new_record?
+
+ page = @wiki.find_or_new_page("Non existing page")
+ assert_kind_of WikiPage, page
+ assert page.new_record?
+ end
+
+ def test_parent_title
+ page = WikiPage.find_by_title('Another_page')
+ assert_nil page.parent_title
+
+ page = WikiPage.find_by_title('Page_with_an_inline_image')
+ assert_equal 'CookBook documentation', page.parent_title
+ end
+
+ def test_assign_parent
+ page = WikiPage.find_by_title('Another_page')
+ page.parent_title = 'CookBook documentation'
+ assert page.save
+ page.reload
+ assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
+ end
+
+ def test_unassign_parent
+ page = WikiPage.find_by_title('Page_with_an_inline_image')
+ page.parent_title = ''
+ assert page.save
+ page.reload
+ assert_nil page.parent
+ end
+
+ def test_parent_validation
+ page = WikiPage.find_by_title('CookBook_documentation')
+
+ # A page that doesn't exist
+ page.parent_title = 'Unknown title'
+ assert !page.save
+ assert_equal I18n.translate('activerecord.errors.messages.invalid'), page.errors.on(:parent_title)
+ # A child page
+ page.parent_title = 'Page_with_an_inline_image'
+ assert !page.save
+ assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
+ # The page itself
+ page.parent_title = 'CookBook_documentation'
+ assert !page.save
+ assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
+
+ page.parent_title = 'Another_page'
+ assert page.save
+ end
+
+ def test_destroy
+ page = WikiPage.find(1)
+ page.destroy
+ assert_nil WikiPage.find_by_id(1)
+ # make sure that page content and its history are deleted
+ assert WikiContent.find_all_by_page_id(1).empty?
+ assert WikiContent.versioned_class.find_all_by_page_id(1).empty?
+ end
+
+ def test_destroy_should_not_nullify_children
+ page = WikiPage.find(2)
+ child_ids = page.child_ids
+ assert child_ids.any?
+ page.destroy
+ assert_nil WikiPage.find_by_id(2)
+
+ children = WikiPage.find_all_by_id(child_ids)
+ assert_equal child_ids.size, children.size
+ children.each do |child|
+ assert_nil child.parent_id
+ end
+ end
+end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class WikiRedirectTest < ActiveSupport::TestCase
+ fixtures :projects, :wikis
+
+ def setup
+ @wiki = Wiki.find(1)
+ @original = WikiPage.create(:wiki => @wiki, :title => 'Original title')
+ end
+
+ def test_create_redirect
+ @original.title = 'New title'
+ assert @original.save
+ @original.reload
+
+ assert_equal 'New_title', @original.title
+ assert @wiki.redirects.find_by_title('Original_title')
+ assert @wiki.find_page('Original title')
+ end
+
+ def test_update_redirect
+ # create a redirect that point to this page
+ assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+
+ @original.title = 'New title'
+ @original.save
+ # make sure the old page now points to the new page
+ assert_equal 'New_title', @wiki.find_page('An old page').title
+ end
+
+ def test_reverse_rename
+ # create a redirect that point to this page
+ assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+
+ @original.title = 'An old page'
+ @original.save
+ assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'An_old_page')
+ assert @wiki.redirects.find_by_title_and_redirects_to('Original_title', 'An_old_page')
+ end
+
+ def test_rename_to_already_redirected
+ assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Other_page')
+
+ @original.title = 'An old page'
+ @original.save
+ # this redirect have to be removed since 'An old page' page now exists
+ assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'Other_page')
+ end
+
+ def test_redirects_removed_when_deleting_page
+ assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title')
+
+ @original.destroy
+ assert !@wiki.redirects.find(:first)
+ end
+end
--- /dev/null
+# encoding: utf-8
+#
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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.dirname(__FILE__) + '/../test_helper'
+
+class WikiTest < ActiveSupport::TestCase
+ fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
+
+ def test_create
+ wiki = Wiki.new(:project => Project.find(2))
+ assert !wiki.save
+ assert_equal 1, wiki.errors.count
+
+ wiki.start_page = "Start page"
+ assert wiki.save
+ end
+
+ def test_update
+ @wiki = Wiki.find(1)
+ @wiki.start_page = "Another start page"
+ assert @wiki.save
+ @wiki.reload
+ assert_equal "Another start page", @wiki.start_page
+ end
+
+ def test_titleize
+ assert_equal 'Page_title_with_CAPITALES', Wiki.titleize('page title with CAPITALES')
+ assert_equal 'テスト', Wiki.titleize('テスト')
+ end
+end
--- /dev/null
+--- !ruby/object:Gem::Specification
+name: rubytree
+version: !ruby/object:Gem::Version
+ version: 0.5.2
+platform: ruby
+authors:
+- Anupam Sengupta
+autorequire: tree
+bindir: bin
+cert_chain: []
+
+date: 2007-12-20 00:00:00 -08:00
+default_executable:
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: hoe
+ type: :runtime
+ version_requirement:
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: 1.3.0
+ version:
+description: "Provides a generic tree data-structure with ability to store keyed node-elements in the tree. The implementation mixes in the Enumerable module. Website: http://rubytree.rubyforge.org/"
+email: anupamsg@gmail.com
+executables: []
+
+extensions: []
+
+extra_rdoc_files:
+- README
+- COPYING
+- ChangeLog
+- History.txt
+files:
+- COPYING
+- ChangeLog
+- History.txt
+- Manifest.txt
+- README
+- Rakefile
+- TODO
+- lib/tree.rb
+- lib/tree/binarytree.rb
+- setup.rb
+- test/test_binarytree.rb
+- test/test_tree.rb
+has_rdoc: true
+homepage: http://rubytree.rubyforge.org/
+licenses: []
+
+post_install_message:
+rdoc_options:
+- --main
+- README
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: "0"
+ version:
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: "0"
+ version:
+requirements: []
+
+rubyforge_project: rubytree
+rubygems_version: 1.3.5
+signing_key:
+specification_version: 2
+summary: Ruby implementation of the Tree data structure.
+test_files:
+- test/test_binarytree.rb
+- test/test_tree.rb
--- /dev/null
+RUBYTREE - http://rubytree.rubyforge.org
+========================================
+
+Copyright (c) 2006, 2007 Anupam Sengupta
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+- Neither the name of the organization nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+2007-12-21 Anupam Sengupta <anupamsg@gmail.com>
+
+ * Rakefile: Added the rcov option to exclude rcov itself from
+ coverage reports.
+
+ * lib/tree.rb: Minor comment changes.
+
+ * test/test_tree.rb: Added the TestTree enclosing module, and
+ renamed tests to meet ZenTest requirements. Also added a few
+ missing test cases.
+
+ * test/test_binarytree.rb: Added the TestTree enclosing Module,
+ and renamed the tests to meet ZenTest requirements.
+
+2007-12-19 Anupam Sengupta <anupamsg@gmail.com>
+
+ * README (Module): Modified the install instructions from source.
+
+ * lib/tree.rb (Tree::TreeNode::initialize): Removed the
+ unnecessary self_initialize method.
+ (Tree::TreeNode): Removed the spurious self_initialize from the
+ protected list.
+ (Module): Updated the minor version number.
+
+ * Rakefile: Fixed a problem with reading the Tree::VERSION for the
+ gem packaging, if any prior version of the gem is already installed.
+
+2007-12-18 Anupam Sengupta <anupamsg@gmail.com>
+
+ * lib/tree.rb: Updated the marshalling logic to correctly handle
+ non-string content.
+ (Tree::TreeNode::createDumpRep): Minor code change to use symbols
+ instead of string key names.
+ (Tree): Version number change to 0.5.0
+ (Tree::TreeNode::hasContent): Minor fix to the comments.
+
+ * test/test_tree.rb (TC_TreeTest::test_breadth_each): Updated test
+ cases for the marshalling logic.
+
+2007-11-12 Anupam Sengupta <anupamsg@gmail.com>
+
+ * test/test_binarytree.rb: Minor documentation correction.
+
+ * lib/tree/binarytree.rb (Tree::BinaryTreeNode::isRightChild):
+ Minor documentation change.
+
+2007-10-10 Anupam Sengupta <anupamsg@gmail.com>
+
+ * README: Restructured the format.
+
+ * Rakefile: Added Hoe related logic. If not present, the Rakefile
+ will default to old behavior.
+
+2007-10-09 Anupam Sengupta <anupamsg@gmail.com>
+
+ * Rakefile: Added setup.rb related tasks. Also added the setup.rb in the PKG_FILES list.
+
+2007-10-01 Anupam Sengupta <anupamsg@gmail.com>
+
+ * Rakefile: Added an optional task for rcov code coverage.
+ Added a dependency for rake in the Gem Specification.
+
+ * test/test_binarytree.rb: Removed the unnecessary dependency on "Person" class.
+
+ * test/test_tree.rb: Removed dependency on the redundant "Person" class.
+ (TC_TreeTest::test_comparator): Added a new test for the spaceship operator.
+ (TC_TreeTest::test_hasContent): Added tests for hasContent? and length methods.
+
+2007-08-30 Anupam Sengupta <anupamsg@gmail.com>
+
+ * test/test_tree.rb (TC_TreeTest::test_preordered_each, TC_TreeTest::test_breadth_each, TC_TreeTest::test_detached_copy):
+ Added new tests for the new functions added to tree.rb.
+
+ * lib/tree.rb (Tree::TreeNode::detached_copy, Tree::TreeNode::preordered_each, Tree::TreeNode::breadth_each):
+ Added new functions for returning a detached copy of the node and
+ for performing breadth first traversal. Also added the pre-ordered
+ traversal function which is an alias of the existing 'each' method.
+
+ * test/test_binarytree.rb (TC_BinaryTreeTest::test_swap_children):
+ Added a test case for the children swap function.
+
+ * lib/tree/binarytree.rb (Tree::BinaryTreeNode::swap_children):
+ Added new function to swap the children. Other minor changes in
+ comments and code.
+
+2007-07-18 Anupam Sengupta <anupamsg@gmail.com>
+
+ * lib/tree/binarytree.rb (Tree::BinaryTreeNode::leftChild /
+ rightChild): Minor cosmetic change on the parameter name.
+
+ * test/testbinarytree.rb (TC_BinaryTreeTest::test_isLeftChild):
+ Minor syntax correction.
+
+ * lib/tree.rb (Tree::TreeNode::depth): Added a tree depth
+ computation method.
+ (Tree::TreeNode::breadth): Added a tree breadth method.
+
+ * test/testtree.rb (TC_TreeTest::test_depth/test_breadth): Added a
+ test for the depth and breadth method.
+
+ * lib/tree/binarytree.rb (Tree::BinaryTreeNode::is*Child):
+ Added tests for determining whether a node is a left or right
+ child.
+
+ * test/testbinarytree.rb: Added the test cases for the binary tree
+ implementation.
+ (TC_BinaryTreeTest::test_is*Child): Added tests for right or left
+ childs.
+
+ * lib/tree/binarytree.rb: Added the binary tree implementation.
+
+2007-07-17 Anupam Sengupta <anupamsg@gmail.com>
+
+ * lib/tree.rb (Tree::TreeNode::parentage): Renamed 'ancestors'
+ method to 'parentage' to avoid clobbering Module.ancestors
+
+2007-07-16 Anupam Sengupta <anupamsg@gmail.com>
+
+ * Rakefile: Added an optional rtags task to generate TAGS file for
+ Emacs.
+
+ * lib/tree.rb (Tree::TreeNode): Added navigation methods for
+ siblings and children. Also added some convenience methods.
+
+2007-07-08 Anupam Sengupta <anupamsg@gmail.com>
+
+ * Rakefile: Added a developer target for generating rdoc for the
+ website.
+
+2007-06-24 Anupam Sengupta <anupamsg@gmail.com>
+
+ * test/testtree.rb, lib/tree.rb: Added the each_leaf traversal method.
+
+ * README: Replaced all occurrances of LICENSE with COPYING
+ and lowercased all instances of the word 'RubyTree'.
+
+ * Rakefile: Replaced all occurrances of LICENSE with COPYING
+
+2007-06-23 Anupam Sengupta <anupamsg@gmail.com>
+
+ * lib/tree.rb (Tree::TreeNode::isLeaf): Added a isLeaf? method.
+
+ * test/testtree.rb (TC_TreeTest::test_removeFromParent): Added
+ test for isLeaf? method
+
+ * Rakefile: Added the LICENSE and ChangeLog to the extra RDoc files.
+
+ * lib/tree.rb: Minor updates to the comments.
+
+ * test/testtree.rb: Added the Copyright and License header.
+
+ * test/person.rb: Added the Copyright and License header.
+
+ * lib/tree.rb: Added the Copyright and License header.
+
+ * Rakefile: Added the LICENSE and Changelog to be part of the RDoc task.
+
+ * README: Added documentation in the README, including install
+ instructions and an example.
+
+ * LICENSE: Added the BSD LICENSE file.
+
+ * Changelog: Added the Changelog file.
--- /dev/null
+= 0.5.2 / 2007-12-21
+
+* Added more test cases and enabled ZenTest compatibility for the test case
+ names.
+
+= 0.5.1 / 2007-12-20
+
+* Minor code refactoring.
+
+= 0.5.0 / 2007-12-18
+
+* Fixed the marshalling code to correctly handle non-string content.
+
+= 0.4.3 / 2007-10-09
+
+* Changes to the build mechanism (now uses Hoe).
+
+= 0.4.2 / 2007-10-01
+
+* Minor code refactoring. Changes in the Rakefile.
--- /dev/null
+COPYING
+ChangeLog
+History.txt
+Manifest.txt
+README
+Rakefile
+TODO
+lib/tree.rb
+lib/tree/binarytree.rb
+setup.rb
+test/test_binarytree.rb
+test/test_tree.rb
--- /dev/null
+\r
+ __ _ _\r
+ /__\_ _| |__ _ _| |_ _ __ ___ ___\r
+ / \// | | | '_ \| | | | __| '__/ _ \/ _ \\r
+ / _ \ |_| | |_) | |_| | |_| | | __/ __/\r
+ \/ \_/\__,_|_.__/ \__, |\__|_| \___|\___|\r
+ |___/\r
+\r
+ (c) 2006, 2007 Anupam Sengupta\r
+ http://rubytree.rubyforge.org\r
+\r
+Rubytree is a simple implementation of the generic Tree data structure. This\r
+implementation is node-centric, where the individual nodes on the tree are the\r
+primary objects and drive the structure.\r
+\r
+== INSTALL:\r
+\r
+Rubytree is an open source project and is hosted at:\r
+\r
+ http://rubytree.rubyforge.org\r
+\r
+Rubytree can be downloaded as a Rubygem or as a tar/zip file from:\r
+\r
+ http://rubyforge.org/frs/?group_id=1215&release_id=8817\r
+\r
+The file-name is one of:\r
+\r
+ rubytree-<VERSION>.gem - The Rubygem\r
+ rubytree-<VERSION>.tgz - GZipped source files\r
+ rubytree-<VERSION>.zip - Zipped source files\r
+\r
+Download the appropriate file-type for your system.\r
+\r
+It is recommended to install Rubytree as a Ruby Gem, as this is an easy way to\r
+keep the version updated, and keep multiple versions of the library available on\r
+your system.\r
+\r
+=== Installing the Gem\r
+\r
+To Install the Gem, from a Terminal/CLI command prompt, issue the command:\r
+\r
+ gem install rubytree\r
+\r
+This should install the gem file for Rubytree. Note that you may need to be a\r
+super-user (root) to successfully install the gem.\r
+\r
+=== Installing from the tgz/zip file\r
+\r
+Extract the archive file (tgz or zip) and run the following command from the\r
+top-level source directory:\r
+\r
+ ruby ./setup.rb\r
+\r
+You may need administrator/super-user privileges to complete the setup using\r
+this method.\r
+\r
+== DOCUMENTATION:\r
+\r
+The primary class for this implementation is Tree::TreeNode. See the\r
+class documentation for an usage example.\r
+\r
+From a command line/terminal prompt, you can issue the following command to view\r
+the text mode ri documentation:\r
+\r
+ ri Tree::TreeNode\r
+\r
+Documentation on the web is available at:\r
+\r
+http://rubytree.rubyforge.org/rdoc\r
+\r
+== EXAMPLE:\r
+\r
+The following code-snippet implements this tree structure:\r
+\r
+ +------------+\r
+ | ROOT |\r
+ +-----+------+\r
+ +-------------+------------+\r
+ | |\r
+ +-------+-------+ +-------+-------+\r
+ | CHILD 1 | | CHILD 2 |\r
+ +-------+-------+ +---------------+\r
+ |\r
+ |\r
+ +-------+-------+\r
+ | GRANDCHILD 1 |\r
+ +---------------+\r
+\r
+ require 'tree'\r
+\r
+ myTreeRoot = Tree::TreeNode.new("ROOT", "Root Content")\r
+\r
+ myTreeRoot << Tree::TreeNode.new("CHILD1", "Child1 Content") << Tree::TreeNode.new("GRANDCHILD1", "GrandChild1 Content")\r
+\r
+ myTreeRoot << Tree::TreeNode.new("CHILD2", "Child2 Content")\r
+\r
+ myTreeRoot.printTree\r
+\r
+ child1 = myTreeRoot["CHILD1"]\r
+\r
+ grandChild1 = myTreeRoot["CHILD1"]["GRANDCHILD1"]\r
+\r
+ siblingsOfChild1Array = child1.siblings\r
+\r
+ immediateChildrenArray = myTreeRoot.children\r
+\r
+ # Process all nodes\r
+\r
+ myTreeRoot.each { |node| node.content.reverse }\r
+\r
+ myTreeRoot.remove!(child1) # Remove the child\r
+\r
+== LICENSE:\r
+\r
+Rubytree is licensed under BSD license.\r
+\r
+Copyright (c) 2006, 2007 Anupam Sengupta\r
+\r
+All rights reserved.\r
+\r
+Redistribution and use in source and binary forms, with or without modification,\r
+are permitted provided that the following conditions are met:\r
+\r
+- Redistributions of source code must retain the above copyright notice, this\r
+ list of conditions and the following disclaimer.\r
+\r
+- Redistributions in binary form must reproduce the above copyright notice, this\r
+ list of conditions and the following disclaimer in the documentation and/or\r
+ other materials provided with the distribution.\r
+\r
+- Neither the name of the organization nor the names of its contributors may\r
+ be used to endorse or promote products derived from this software without\r
+ specific prior written permission.\r
+\r
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\r
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\r
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\r
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\r
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\r
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+\r
+\r
+(Document Revision: $Revision: 1.16 $ by $Author: anupamsg $)\r
--- /dev/null
+# Rakefile
+#
+# $Revision: 1.27 $ by $Author: anupamsg $
+# $Name: $
+#
+# Copyright (c) 2006, 2007 Anupam Sengupta
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# - Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+#
+# - Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+require 'rubygems'
+require 'rake/gempackagetask'
+
+require 'rake/clean'
+require 'rake/packagetask'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'fileutils'
+
+# General Stuff ####################################################
+
+$:.insert 0, File.expand_path( File.join( File.dirname(__FILE__), 'lib' ) )
+require 'tree' # To read the version information.
+
+PKG_NAME = "rubytree"
+PKG_VERSION = Tree::VERSION
+PKG_FULLNAME = PKG_NAME + "-" + PKG_VERSION
+PKG_SUMMARY = "Ruby implementation of the Tree data structure."
+PKG_DESCRIPTION = <<-END
+ Provides a generic tree data-structure with ability to
+ store keyed node-elements in the tree. The implementation
+ mixes in the Enumerable module.
+
+ Website: http://rubytree.rubyforge.org/
+ END
+
+PKG_FILES = FileList[
+ '[A-Z]*',
+ '*.rb',
+ 'lib/**/*.rb',
+ 'test/**/*.rb'
+ ]
+
+# Default is to create a rubygem.
+desc "Default Task"
+task :default => :gem
+
+begin # Try loading hoe
+ require 'hoe'
+ # If Hoe is found, use it to define tasks
+ # =======================================
+ Hoe.new(PKG_NAME, PKG_VERSION) do |p|
+ p.rubyforge_name = PKG_NAME
+ p.author = "Anupam Sengupta"
+ p.email = "anupamsg@gmail.com"
+ p.summary = PKG_SUMMARY
+ p.description = PKG_DESCRIPTION
+ p.url = "http://rubytree.rubyforge.org/"
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
+ p.remote_rdoc_dir = 'rdoc'
+ p.need_tar = true
+ p.need_zip = true
+ p.test_globs = ['test/test_*.rb']
+ p.spec_extras = {
+ :has_rdoc => true,
+ :platform => Gem::Platform::RUBY,
+ :has_rdoc => true,
+ :extra_rdoc_files => ['README', 'COPYING', 'ChangeLog', 'History.txt'],
+ :rdoc_options => ['--main', 'README'],
+ :autorequire => 'tree'
+ }
+ end
+
+rescue LoadError # If Hoe is not found
+ # If Hoe is not found, then use the usual Gemspec based Rake tasks
+ # ================================================================
+ spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.author = "Anupam Sengupta"
+ s.email = "anupamsg@gmail.com"
+ s.homepage = "http://rubytree.rubyforge.org/"
+ s.rubyforge_project = 'rubytree'
+ s.summary = PKG_SUMMARY
+ s.add_dependency('rake', '>= 0.7.2')
+ s.description = PKG_DESCRIPTION
+ s.has_rdoc = true
+ s.extra_rdoc_files = ['README', 'COPYING', 'ChangeLog']
+ s.autorequire = "tree"
+ s.files = PKG_FILES.to_a
+ s.test_files = Dir.glob('test/test*.rb')
+ end
+
+ desc "Prepares for installation"
+ task :prepare do
+ ruby "setup.rb config"
+ ruby "setup.rb setup"
+ end
+
+ desc "Installs the package #{PKG_NAME}"
+ task :install => [:prepare] do
+ ruby "setup.rb install"
+ end
+
+ Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_zip = true
+ pkg.need_tar = true
+ end
+
+ Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList['test/test*.rb']
+ t.verbose = true
+ end
+
+end # End loading Hoerc
+# ================================================================
+
+
+# The following tasks are loaded independently of Hoe
+
+# Hoe's rdoc task is ugly.
+Rake::RDocTask.new(:docs) do |rd|
+ rd.rdoc_files.include("README", "COPYING", "ChangeLog", "lib/**/*.rb")
+ rd.rdoc_dir = 'doc'
+ rd.title = "#{PKG_FULLNAME} Documentation"
+
+ # Use the template only if it is present, otherwise, the standard template is
+ # used.
+ template = "../allison/allison.rb"
+ rd.template = template if File.file?(template)
+
+ rd.options << '--line-numbers' << '--inline-source'
+end
+
+# Optional TAGS Task.
+# Needs https://rubyforge.org/projects/rtagstask/
+begin
+ require 'rtagstask'
+ RTagsTask.new do |rd|
+ rd.vi = false
+ end
+rescue LoadError
+end
+
+# Optional RCOV Task
+# Needs http://eigenclass.org/hiki/rcov
+begin
+ require 'rcov/rcovtask'
+ Rcov::RcovTask.new do |t|
+ t.test_files = FileList['test/test*.rb']
+ t.rcov_opts << "--exclude 'rcov.rb'" # rcov itself should not be profiled
+ # t.verbose = true # uncomment to see the executed commands
+ end
+rescue LoadError
+end
+
+#Rakefile,v $
+# Revision 1.21 2007/07/21 05:14:43 anupamsg
+# Added a VERSION constant to the Tree module,
+# and using the same in the Rakefile.
+#
+# Revision 1.20 2007/07/21 03:24:25 anupamsg
+# Minor edits to parameter names. User visible functionality does not change.
+#
+# Revision 1.19 2007/07/19 02:16:01 anupamsg
+# Release 0.4.0 (and minor fix in Rakefile).
+#
+# Revision 1.18 2007/07/18 20:15:06 anupamsg
+# Added two predicate methods in BinaryTreeNode to determine whether a node
+# is a left or a right node.
+#
+# Revision 1.17 2007/07/18 07:17:34 anupamsg
+# Fixed a issue where TreeNode.ancestors was shadowing Module.ancestors. This method
+# has been renamed to TreeNode.parentage.
+#
+# Revision 1.16 2007/07/17 05:34:03 anupamsg
+# Added an optional tags Rake-task for generating the TAGS file for Emacs.
+#
+# Revision 1.15 2007/07/17 04:42:45 anupamsg
+# Minor fixes to the Rakefile.
+#
+# Revision 1.14 2007/07/17 03:39:28 anupamsg
+# Moved the CVS Log keyword to end of the files.
+#
--- /dev/null
+# -*- mode: outline; coding: utf-8-unix; -*-
+
+* Add logic in Rakefile to read the file list from Manifest.txt file.
+
+* Add a YAML export method to the TreeNode class.
+
+
--- /dev/null
+# tree.rb
+#
+# $Revision: 1.29 $ by $Author: anupamsg $
+# $Name: $
+#
+# = tree.rb - Generic Multi-way Tree implementation
+#
+# Provides a generic tree data structure with ability to
+# store keyed node elements in the tree. The implementation
+# mixes in the Enumerable module.
+#
+# Author:: Anupam Sengupta (anupamsg@gmail.com)
+#
+
+# Copyright (c) 2006, 2007 Anupam Sengupta
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# - Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+#
+# - Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+# This module provides a TreeNode class which is the primary class for all
+# nodes represented in the Tree.
+# This module mixes in the Enumerable module.
+module Tree
+
+ # Rubytree Package Version
+ VERSION = '0.5.2'
+
+ # == TreeNode Class Description
+ #
+ # The node class for the tree representation. the nodes are named and have a
+ # place-holder for the node data (i.e., the `content' of the node). The node
+ # names are expected to be unique. In addition, the node provides navigation
+ # methods to traverse the tree.
+ #
+ # The nodes can have any number of child nodes attached to it. Note that while
+ # this implementation does not support directed graphs, the class itself makes
+ # no restrictions on associating a node's CONTENT with multiple parent nodes.
+ #
+ #
+ # == Example
+ #
+ # The following code-snippet implements this tree structure:
+ #
+ # +------------+
+ # | ROOT |
+ # +-----+------+
+ # +-------------+------------+
+ # | |
+ # +-------+-------+ +-------+-------+
+ # | CHILD 1 | | CHILD 2 |
+ # +-------+-------+ +---------------+
+ # |
+ # |
+ # +-------+-------+
+ # | GRANDCHILD 1 |
+ # +---------------+
+ #
+ # require 'tree'
+ #
+ # myTreeRoot = Tree::TreeNode.new("ROOT", "Root Content")
+ #
+ # myTreeRoot << Tree::TreeNode.new("CHILD1", "Child1 Content") << Tree::TreeNode.new("GRANDCHILD1", "GrandChild1 Content")
+ #
+ # myTreeRoot << Tree::TreeNode.new("CHILD2", "Child2 Content")
+ #
+ # myTreeRoot.printTree
+ #
+ # child1 = myTreeRoot["CHILD1"]
+ #
+ # grandChild1 = myTreeRoot["CHILD1"]["GRANDCHILD1"]
+ #
+ # siblingsOfChild1Array = child1.siblings
+ #
+ # immediateChildrenArray = myTreeRoot.children
+ #
+ # # Process all nodes
+ #
+ # myTreeRoot.each { |node| node.content.reverse }
+ #
+ # myTreeRoot.remove!(child1) # Remove the child
+ class TreeNode
+ include Enumerable
+
+ attr_reader :content, :name, :parent
+ attr_writer :content
+
+ # Constructor which expects the name of the node
+ #
+ # Name of the node is expected to be unique across the
+ # tree.
+ #
+ # The content can be of any type, and is defaulted to _nil_.
+ def initialize(name, content = nil)
+ raise "Node name HAS to be provided" if name == nil
+ @name = name
+ @content = content
+ self.setAsRoot!
+
+ @childrenHash = Hash.new
+ @children = []
+ end
+
+ # Returns a copy of this node, with the parent and children links removed.
+ def detached_copy
+ Tree::TreeNode.new(@name, @content ? @content.clone : nil)
+ end
+
+ # Print the string representation of this node.
+ def to_s
+ "Node Name: #{@name}" +
+ " Content: " + (@content || "<Empty>") +
+ " Parent: " + (isRoot?() ? "<None>" : @parent.name) +
+ " Children: #{@children.length}" +
+ " Total Nodes: #{size()}"
+ end
+
+ # Returns an array of ancestors in reversed order (the first element is the
+ # immediate parent). Returns nil if this is a root node.
+ def parentage
+ return nil if isRoot?
+
+ parentageArray = []
+ prevParent = self.parent
+ while (prevParent)
+ parentageArray << prevParent
+ prevParent = prevParent.parent
+ end
+
+ parentageArray
+ end
+
+ # Protected method to set the parent node.
+ # This method should NOT be invoked by client code.
+ def parent=(parent)
+ @parent = parent
+ end
+
+ # Convenience synonym for TreeNode#add method. This method allows a convenient
+ # method to add children hierarchies in the tree.
+ #
+ # E.g. root << child << grand_child
+ def <<(child)
+ add(child)
+ end
+
+ # Adds the specified child node to the receiver node. The child node's
+ # parent is set to be the receiver. The child is added as the last child in
+ # the current list of children for the receiver node.
+ def add(child)
+ raise "Child already added" if @childrenHash.has_key?(child.name)
+
+ @childrenHash[child.name] = child
+ @children << child
+ child.parent = self
+ return child
+
+ end
+
+ # Removes the specified child node from the receiver node. The removed
+ # children nodes are orphaned but available if an alternate reference
+ # exists.
+ #
+ # Returns the child node.
+ def remove!(child)
+ @childrenHash.delete(child.name)
+ @children.delete(child)
+ child.setAsRoot! unless child == nil
+ return child
+ end
+
+ # Removes this node from its parent. If this is the root node, then does
+ # nothing.
+ def removeFromParent!
+ @parent.remove!(self) unless isRoot?
+ end
+
+ # Removes all children from the receiver node.
+ def removeAll!
+ for child in @children
+ child.setAsRoot!
+ end
+ @childrenHash.clear
+ @children.clear
+ self
+ end
+
+ # Indicates whether this node has any associated content.
+ def hasContent?
+ @content != nil
+ end
+
+ # Protected method which sets this node as a root node.
+ def setAsRoot!
+ @parent = nil
+ end
+
+ # Indicates whether this node is a root node. Note that
+ # orphaned children will also be reported as root nodes.
+ def isRoot?
+ @parent == nil
+ end
+
+ # Indicates whether this node has any immediate child nodes.
+ def hasChildren?
+ @children.length != 0
+ end
+
+ # Indicates whether this node is a 'leaf' - i.e., one without
+ # any children
+ def isLeaf?
+ !hasChildren?
+ end
+
+ # Returns an array of all the immediate children. If a block is given,
+ # yields each child node to the block.
+ def children
+ if block_given?
+ @children.each {|child| yield child}
+ else
+ @children
+ end
+ end
+
+ # Returns the first child of this node. Will return nil if no children are
+ # present.
+ def firstChild
+ children.first
+ end
+
+ # Returns the last child of this node. Will return nil if no children are
+ # present.
+ def lastChild
+ children.last
+ end
+
+ # Returns every node (including the receiver node) from the tree to the
+ # specified block. The traversal is depth first and from left to right in
+ # pre-ordered sequence.
+ def each &block
+ yield self
+ children { |child| child.each(&block) }
+ end
+
+ # Traverses the tree in a pre-ordered sequence. This is equivalent to
+ # TreeNode#each
+ def preordered_each &block
+ each(&block)
+ end
+
+ # Performs breadth first traversal of the tree rooted at this node. The
+ # traversal in a given level is from left to right.
+ def breadth_each &block
+ node_queue = [self] # Create a queue with self as the initial entry
+
+ # Use a queue to do breadth traversal
+ until node_queue.empty?
+ node_to_traverse = node_queue.shift
+ yield node_to_traverse
+ # Enqueue the children from left to right.
+ node_to_traverse.children { |child| node_queue.push child }
+ end
+ end
+
+ # Yields all leaf nodes from this node to the specified block. May yield
+ # this node as well if this is a leaf node. Leaf traversal depth first and
+ # left to right.
+ def each_leaf &block
+ self.each { |node| yield(node) if node.isLeaf? }
+ end
+
+ # Returns the requested node from the set of immediate children.
+ #
+ # If the parameter is _numeric_, then the in-sequence array of children is
+ # accessed (see Tree#children). If the parameter is not _numeric_, then it
+ # is assumed to be the *name* of the child node to be returned.
+ def [](name_or_index)
+ raise "Name_or_index needs to be provided" if name_or_index == nil
+
+ if name_or_index.kind_of?(Integer)
+ @children[name_or_index]
+ else
+ @childrenHash[name_or_index]
+ end
+ end
+
+ # Returns the total number of nodes in this tree, rooted at the receiver
+ # node.
+ def size
+ @children.inject(1) {|sum, node| sum + node.size}
+ end
+
+ # Convenience synonym for Tree#size
+ def length
+ size()
+ end
+
+ # Pretty prints the tree starting with the receiver node.
+ def printTree(level = 0)
+
+ if isRoot?
+ print "*"
+ else
+ print "|" unless parent.isLastSibling?
+ print(' ' * (level - 1) * 4)
+ print(isLastSibling? ? "+" : "|")
+ print "---"
+ print(hasChildren? ? "+" : ">")
+ end
+
+ puts " #{name}"
+
+ children { |child| child.printTree(level + 1)}
+ end
+
+ # Returns the root for this tree. Root's root is itself.
+ def root
+ root = self
+ root = root.parent while !root.isRoot?
+ root
+ end
+
+ # Returns the first sibling for this node. If this is the root node, returns
+ # itself.
+ def firstSibling
+ if isRoot?
+ self
+ else
+ parent.children.first
+ end
+ end
+
+ # Returns true if this node is the first sibling.
+ def isFirstSibling?
+ firstSibling == self
+ end
+
+ # Returns the last sibling for this node. If this node is the root, returns
+ # itself.
+ def lastSibling
+ if isRoot?
+ self
+ else
+ parent.children.last
+ end
+ end
+
+ # Returns true if his node is the last sibling
+ def isLastSibling?
+ lastSibling == self
+ end
+
+ # Returns an array of siblings for this node.
+ # If a block is provided, yields each of the sibling
+ # nodes to the block. The root always has nil siblings.
+ def siblings
+ return nil if isRoot?
+ if block_given?
+ for sibling in parent.children
+ yield sibling if sibling != self
+ end
+ else
+ siblings = []
+ parent.children {|sibling| siblings << sibling if sibling != self}
+ siblings
+ end
+ end
+
+ # Returns true if this node is the only child of its parent
+ def isOnlyChild?
+ parent.children.size == 1
+ end
+
+ # Returns the next sibling for this node. Will return nil if no subsequent
+ # node is present.
+ def nextSibling
+ if myidx = parent.children.index(self)
+ parent.children.at(myidx + 1)
+ end
+ end
+
+ # Returns the previous sibling for this node. Will return nil if no
+ # subsequent node is present.
+ def previousSibling
+ if myidx = parent.children.index(self)
+ parent.children.at(myidx - 1) if myidx > 0
+ end
+ end
+
+ # Provides a comparision operation for the nodes. Comparision
+ # is based on the natural character-set ordering for the
+ # node names.
+ def <=>(other)
+ return +1 if other == nil
+ self.name <=> other.name
+ end
+
+ # Freezes all nodes in the tree
+ def freezeTree!
+ each {|node| node.freeze}
+ end
+
+ # Creates the marshal-dump represention of the tree rooted at this node.
+ def marshal_dump
+ self.collect { |node| node.createDumpRep }
+ end
+
+ # Creates a dump representation and returns the same as a hash.
+ def createDumpRep
+ { :name => @name, :parent => (isRoot? ? nil : @parent.name), :content => Marshal.dump(@content)}
+ end
+
+ # Loads a marshalled dump of the tree and returns the root node of the
+ # reconstructed tree. See the Marshal class for additional details.
+ def marshal_load(dumped_tree_array)
+ nodes = { }
+ for node_hash in dumped_tree_array do
+ name = node_hash[:name]
+ parent_name = node_hash[:parent]
+ content = Marshal.load(node_hash[:content])
+
+ if parent_name then
+ nodes[name] = current_node = Tree::TreeNode.new(name, content)
+ nodes[parent_name].add current_node
+ else
+ # This is the root node, hence initialize self.
+ initialize(name, content)
+
+ nodes[name] = self # Add self to the list of nodes
+ end
+ end
+ end
+
+ # Returns depth of the tree from this node. A single leaf node has a
+ # depth of 1.
+ def depth
+ return 1 if isLeaf?
+ 1 + @children.collect { |child| child.depth }.max
+ end
+
+ # Returns breadth of the tree at this node level. A single node has a
+ # breadth of 1.
+ def breadth
+ return 1 if isRoot?
+ parent.children.size
+ end
+
+ protected :parent=, :setAsRoot!, :createDumpRep
+
+ end
+end
+
+# $Log: tree.rb,v $
+# Revision 1.29 2007/12/22 00:28:59 anupamsg
+# Added more test cases, and enabled ZenTest compatibility.
+#
+# Revision 1.28 2007/12/20 03:19:33 anupamsg
+# * README (Module): Modified the install instructions from source.
+# (Module): Updated the minor version number.
+#
+# Revision 1.27 2007/12/20 03:00:03 anupamsg
+# Minor code changes. Removed self_initialize from the protected methods' list.
+#
+# Revision 1.26 2007/12/20 02:50:04 anupamsg
+# (Tree::TreeNode): Removed the spurious self_initialize from the protected list.
+#
+# Revision 1.25 2007/12/19 20:28:05 anupamsg
+# Removed the unnecesary self_initialize method.
+#
+# Revision 1.24 2007/12/19 06:39:17 anupamsg
+# Removed the unnecessary field and record separator constants. Also updated the
+# history.txt file.
+#
+# Revision 1.23 2007/12/19 06:25:00 anupamsg
+# (Tree::TreeNode): Minor fix to the comments. Also fixed the private/protected
+# scope issue with the createDumpRep method.
+#
+# Revision 1.22 2007/12/19 06:22:03 anupamsg
+# Updated the marshalling logic to correctly handle non-string content. This
+# should fix the bug # 15614 ("When dumping with an Object as the content, you get
+# a delimiter collision")
+#
+# Revision 1.21 2007/12/19 02:24:17 anupamsg
+# Updated the marshalling logic to handle non-string contents on the nodes.
+#
+# Revision 1.20 2007/10/10 08:42:57 anupamsg
+# Release 0.4.3
+#
+# Revision 1.19 2007/08/31 01:16:27 anupamsg
+# Added breadth and pre-order traversals for the tree. Also added a method
+# to return the detached copy of a node from the tree.
+#
+# Revision 1.18 2007/07/21 05:14:44 anupamsg
+# Added a VERSION constant to the Tree module,
+# and using the same in the Rakefile.
+#
+# Revision 1.17 2007/07/21 03:24:25 anupamsg
+# Minor edits to parameter names. User visible functionality does not change.
+#
+# Revision 1.16 2007/07/18 23:38:55 anupamsg
+# Minor updates to tree.rb
+#
+# Revision 1.15 2007/07/18 22:11:50 anupamsg
+# Added depth and breadth methods for the TreeNode.
+#
+# Revision 1.14 2007/07/18 19:33:27 anupamsg
+# Added a new binary tree implementation.
+#
+# Revision 1.13 2007/07/18 07:17:34 anupamsg
+# Fixed a issue where TreeNode.ancestors was shadowing Module.ancestors. This method
+# has been renamed to TreeNode.parentage.
+#
+# Revision 1.12 2007/07/17 03:39:28 anupamsg
+# Moved the CVS Log keyword to end of the files.
+#
--- /dev/null
+# binarytree.rb
+#
+# $Revision: 1.5 $ by $Author: anupamsg $
+# $Name: $
+#
+# = binarytree.rb - Binary Tree implementation
+#
+# Provides a generic tree data structure with ability to
+# store keyed node elements in the tree. The implementation
+# mixes in the Enumerable module.
+#
+# Author:: Anupam Sengupta (anupamsg@gmail.com)
+#
+
+# Copyright (c) 2007 Anupam Sengupta
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# - Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+#
+# - Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+require 'tree'
+
+module Tree
+
+ # Provides a Binary tree implementation. This tree node allows only two child
+ # nodes (left and right childs). It also provides direct access to the left
+ # and right children, including assignment to the same.
+ class BinaryTreeNode < TreeNode
+
+ # Adds the specified child node to the receiver node. The child node's
+ # parent is set to be the receiver. The child nodes are added in the order
+ # of addition, i.e., the first child added becomes the left node, and the
+ # second child will be the second node.
+ # If only one child is present, then this will be the left child.
+ def add(child)
+ raise "Already has two child nodes" if @children.size == 2
+
+ super(child)
+ end
+
+ # Returns the left child node. Note that
+ # left Child == first Child
+ def leftChild
+ children.first
+ end
+
+ # Returns the right child node. Note that
+ # right child == last child unless there is only one child.
+ # Returns nil if the right child does not exist.
+ def rightChild
+ children[1]
+ end
+
+ # Sets the left child. If a previous child existed, it is replaced.
+ def leftChild=(child)
+ @children[0] = child
+ @childrenHash[child.name] = child if child # Assign the name mapping
+ end
+
+ # Sets the right child. If a previous child existed, it is replaced.
+ def rightChild=(child)
+ @children[1] = child
+ @childrenHash[child.name] = child if child # Assign the name mapping
+ end
+
+ # Returns true if this is the left child of its parent. Always returns false
+ # if this is the root node.
+ def isLeftChild?
+ return nil if isRoot?
+ self == parent.leftChild
+ end
+
+ # Returns true if this is the right child of its parent. Always returns false
+ # if this is the root node.
+ def isRightChild?
+ return nil if isRoot?
+ self == parent.rightChild
+ end
+
+ # Swaps the left and right children with each other
+ def swap_children
+ tempChild = leftChild
+ self.leftChild= rightChild
+ self.rightChild= tempChild
+ end
+ end
+
+end
+
+# $Log: binarytree.rb,v $
+# Revision 1.5 2007/12/18 23:11:29 anupamsg
+# Minor documentation changes in the binarytree class.
+#
+# Revision 1.4 2007/08/30 22:08:58 anupamsg
+# Added a new swap_children method for Binary Tree. Also added minor
+# documentation and test updates.
+#
+# Revision 1.3 2007/07/21 03:24:25 anupamsg
+# Minor edits to parameter names. User visible functionality does not change.
+#
+# Revision 1.2 2007/07/18 20:15:06 anupamsg
+# Added two predicate methods in BinaryTreeNode to determine whether a node
+# is a left or a right node.
+#
+# Revision 1.1 2007/07/18 19:33:27 anupamsg
+# Added a new binary tree implementation.
+#
--- /dev/null
+#
+# setup.rb
+#
+# Copyright (c) 2000-2005 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU LGPL, Lesser General Public License version 2.1.
+#
+
+unless Enumerable.method_defined?(:map) # Ruby 1.4.6
+ module Enumerable
+ alias map collect
+ end
+end
+
+unless File.respond_to?(:read) # Ruby 1.6
+ def File.read(fname)
+ open(fname) {|f|
+ return f.read
+ }
+ end
+end
+
+unless Errno.const_defined?(:ENOTEMPTY) # Windows?
+ module Errno
+ class ENOTEMPTY
+ # We do not raise this exception, implementation is not needed.
+ end
+ end
+end
+
+def File.binread(fname)
+ open(fname, 'rb') {|f|
+ return f.read
+ }
+end
+
+# for corrupted Windows' stat(2)
+def File.dir?(path)
+ File.directory?((path[-1,1] == '/') ? path : path + '/')
+end
+
+
+class ConfigTable
+
+ include Enumerable
+
+ def initialize(rbconfig)
+ @rbconfig = rbconfig
+ @items = []
+ @table = {}
+ # options
+ @install_prefix = nil
+ @config_opt = nil
+ @verbose = true
+ @no_harm = false
+ end
+
+ attr_accessor :install_prefix
+ attr_accessor :config_opt
+
+ attr_writer :verbose
+
+ def verbose?
+ @verbose
+ end
+
+ attr_writer :no_harm
+
+ def no_harm?
+ @no_harm
+ end
+
+ def [](key)
+ lookup(key).resolve(self)
+ end
+
+ def []=(key, val)
+ lookup(key).set val
+ end
+
+ def names
+ @items.map {|i| i.name }
+ end
+
+ def each(&block)
+ @items.each(&block)
+ end
+
+ def key?(name)
+ @table.key?(name)
+ end
+
+ def lookup(name)
+ @table[name] or setup_rb_error "no such config item: #{name}"
+ end
+
+ def add(item)
+ @items.push item
+ @table[item.name] = item
+ end
+
+ def remove(name)
+ item = lookup(name)
+ @items.delete_if {|i| i.name == name }
+ @table.delete_if {|name, i| i.name == name }
+ item
+ end
+
+ def load_script(path, inst = nil)
+ if File.file?(path)
+ MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
+ end
+ end
+
+ def savefile
+ '.config'
+ end
+
+ def load_savefile
+ begin
+ File.foreach(savefile()) do |line|
+ k, v = *line.split(/=/, 2)
+ self[k] = v.strip
+ end
+ rescue Errno::ENOENT
+ setup_rb_error $!.message + "\n#{File.basename($0)} config first"
+ end
+ end
+
+ def save
+ @items.each {|i| i.value }
+ File.open(savefile(), 'w') {|f|
+ @items.each do |i|
+ f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
+ end
+ }
+ end
+
+ def load_standard_entries
+ standard_entries(@rbconfig).each do |ent|
+ add ent
+ end
+ end
+
+ def standard_entries(rbconfig)
+ c = rbconfig
+
+ rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
+
+ major = c['MAJOR'].to_i
+ minor = c['MINOR'].to_i
+ teeny = c['TEENY'].to_i
+ version = "#{major}.#{minor}"
+
+ # ruby ver. >= 1.4.4?
+ newpath_p = ((major >= 2) or
+ ((major == 1) and
+ ((minor >= 5) or
+ ((minor == 4) and (teeny >= 4)))))
+
+ if c['rubylibdir']
+ # V > 1.6.3
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = c['rubylibdir']
+ librubyverarch = c['archdir']
+ siteruby = c['sitedir']
+ siterubyver = c['sitelibdir']
+ siterubyverarch = c['sitearchdir']
+ elsif newpath_p
+ # 1.4.4 <= V <= 1.6.3
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+ siteruby = c['sitedir']
+ siterubyver = "$siteruby/#{version}"
+ siterubyverarch = "$siterubyver/#{c['arch']}"
+ else
+ # V < 1.4.4
+ libruby = "#{c['prefix']}/lib/ruby"
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+ siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
+ siterubyver = siteruby
+ siterubyverarch = "$siterubyver/#{c['arch']}"
+ end
+ parameterize = lambda {|path|
+ path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
+ }
+
+ if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
+ makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
+ else
+ makeprog = 'make'
+ end
+
+ [
+ ExecItem.new('installdirs', 'std/site/home',
+ 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
+ {|val, table|
+ case val
+ when 'std'
+ table['rbdir'] = '$librubyver'
+ table['sodir'] = '$librubyverarch'
+ when 'site'
+ table['rbdir'] = '$siterubyver'
+ table['sodir'] = '$siterubyverarch'
+ when 'home'
+ setup_rb_error '$HOME was not set' unless ENV['HOME']
+ table['prefix'] = ENV['HOME']
+ table['rbdir'] = '$libdir/ruby'
+ table['sodir'] = '$libdir/ruby'
+ end
+ },
+ PathItem.new('prefix', 'path', c['prefix'],
+ 'path prefix of target environment'),
+ PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
+ 'the directory for commands'),
+ PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
+ 'the directory for libraries'),
+ PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
+ 'the directory for shared data'),
+ PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
+ 'the directory for man pages'),
+ PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
+ 'the directory for system configuration files'),
+ PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
+ 'the directory for local state data'),
+ PathItem.new('libruby', 'path', libruby,
+ 'the directory for ruby libraries'),
+ PathItem.new('librubyver', 'path', librubyver,
+ 'the directory for standard ruby libraries'),
+ PathItem.new('librubyverarch', 'path', librubyverarch,
+ 'the directory for standard ruby extensions'),
+ PathItem.new('siteruby', 'path', siteruby,
+ 'the directory for version-independent aux ruby libraries'),
+ PathItem.new('siterubyver', 'path', siterubyver,
+ 'the directory for aux ruby libraries'),
+ PathItem.new('siterubyverarch', 'path', siterubyverarch,
+ 'the directory for aux ruby binaries'),
+ PathItem.new('rbdir', 'path', '$siterubyver',
+ 'the directory for ruby scripts'),
+ PathItem.new('sodir', 'path', '$siterubyverarch',
+ 'the directory for ruby extentions'),
+ PathItem.new('rubypath', 'path', rubypath,
+ 'the path to set to #! line'),
+ ProgramItem.new('rubyprog', 'name', rubypath,
+ 'the ruby program using for installation'),
+ ProgramItem.new('makeprog', 'name', makeprog,
+ 'the make program to compile ruby extentions'),
+ SelectItem.new('shebang', 'all/ruby/never', 'ruby',
+ 'shebang line (#!) editing mode'),
+ BoolItem.new('without-ext', 'yes/no', 'no',
+ 'does not compile/install ruby extentions')
+ ]
+ end
+ private :standard_entries
+
+ def load_multipackage_entries
+ multipackage_entries().each do |ent|
+ add ent
+ end
+ end
+
+ def multipackage_entries
+ [
+ PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
+ 'package names that you want to install'),
+ PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
+ 'package names that you do not want to install')
+ ]
+ end
+ private :multipackage_entries
+
+ ALIASES = {
+ 'std-ruby' => 'librubyver',
+ 'stdruby' => 'librubyver',
+ 'rubylibdir' => 'librubyver',
+ 'archdir' => 'librubyverarch',
+ 'site-ruby-common' => 'siteruby', # For backward compatibility
+ 'site-ruby' => 'siterubyver', # For backward compatibility
+ 'bin-dir' => 'bindir',
+ 'bin-dir' => 'bindir',
+ 'rb-dir' => 'rbdir',
+ 'so-dir' => 'sodir',
+ 'data-dir' => 'datadir',
+ 'ruby-path' => 'rubypath',
+ 'ruby-prog' => 'rubyprog',
+ 'ruby' => 'rubyprog',
+ 'make-prog' => 'makeprog',
+ 'make' => 'makeprog'
+ }
+
+ def fixup
+ ALIASES.each do |ali, name|
+ @table[ali] = @table[name]
+ end
+ @items.freeze
+ @table.freeze
+ @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
+ end
+
+ def parse_opt(opt)
+ m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
+ m.to_a[1,2]
+ end
+
+ def dllext
+ @rbconfig['DLEXT']
+ end
+
+ def value_config?(name)
+ lookup(name).value?
+ end
+
+ class Item
+ def initialize(name, template, default, desc)
+ @name = name.freeze
+ @template = template
+ @value = default
+ @default = default
+ @description = desc
+ end
+
+ attr_reader :name
+ attr_reader :description
+
+ attr_accessor :default
+ alias help_default default
+
+ def help_opt
+ "--#{@name}=#{@template}"
+ end
+
+ def value?
+ true
+ end
+
+ def value
+ @value
+ end
+
+ def resolve(table)
+ @value.gsub(%r<\$([^/]+)>) { table[$1] }
+ end
+
+ def set(val)
+ @value = check(val)
+ end
+
+ private
+
+ def check(val)
+ setup_rb_error "config: --#{name} requires argument" unless val
+ val
+ end
+ end
+
+ class BoolItem < Item
+ def config_type
+ 'bool'
+ end
+
+ def help_opt
+ "--#{@name}"
+ end
+
+ private
+
+ def check(val)
+ return 'yes' unless val
+ case val
+ when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
+ when /\An(o)?\z/i, /\Af(alse)\z/i then 'no'
+ else
+ setup_rb_error "config: --#{@name} accepts only yes/no for argument"
+ end
+ end
+ end
+
+ class PathItem < Item
+ def config_type
+ 'path'
+ end
+
+ private
+
+ def check(path)
+ setup_rb_error "config: --#{@name} requires argument" unless path
+ path[0,1] == '$' ? path : File.expand_path(path)
+ end
+ end
+
+ class ProgramItem < Item
+ def config_type
+ 'program'
+ end
+ end
+
+ class SelectItem < Item
+ def initialize(name, selection, default, desc)
+ super
+ @ok = selection.split('/')
+ end
+
+ def config_type
+ 'select'
+ end
+
+ private
+
+ def check(val)
+ unless @ok.include?(val.strip)
+ setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
+ end
+ val.strip
+ end
+ end
+
+ class ExecItem < Item
+ def initialize(name, selection, desc, &block)
+ super name, selection, nil, desc
+ @ok = selection.split('/')
+ @action = block
+ end
+
+ def config_type
+ 'exec'
+ end
+
+ def value?
+ false
+ end
+
+ def resolve(table)
+ setup_rb_error "$#{name()} wrongly used as option value"
+ end
+
+ undef set
+
+ def evaluate(val, table)
+ v = val.strip.downcase
+ unless @ok.include?(v)
+ setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
+ end
+ @action.call v, table
+ end
+ end
+
+ class PackageSelectionItem < Item
+ def initialize(name, template, default, help_default, desc)
+ super name, template, default, desc
+ @help_default = help_default
+ end
+
+ attr_reader :help_default
+
+ def config_type
+ 'package'
+ end
+
+ private
+
+ def check(val)
+ unless File.dir?("packages/#{val}")
+ setup_rb_error "config: no such package: #{val}"
+ end
+ val
+ end
+ end
+
+ class MetaConfigEnvironment
+ def initialize(config, installer)
+ @config = config
+ @installer = installer
+ end
+
+ def config_names
+ @config.names
+ end
+
+ def config?(name)
+ @config.key?(name)
+ end
+
+ def bool_config?(name)
+ @config.lookup(name).config_type == 'bool'
+ end
+
+ def path_config?(name)
+ @config.lookup(name).config_type == 'path'
+ end
+
+ def value_config?(name)
+ @config.lookup(name).config_type != 'exec'
+ end
+
+ def add_config(item)
+ @config.add item
+ end
+
+ def add_bool_config(name, default, desc)
+ @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
+ end
+
+ def add_path_config(name, default, desc)
+ @config.add PathItem.new(name, 'path', default, desc)
+ end
+
+ def set_config_default(name, default)
+ @config.lookup(name).default = default
+ end
+
+ def remove_config(name)
+ @config.remove(name)
+ end
+
+ # For only multipackage
+ def packages
+ raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
+ @installer.packages
+ end
+
+ # For only multipackage
+ def declare_packages(list)
+ raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
+ @installer.packages = list
+ end
+ end
+
+end # class ConfigTable
+
+
+# This module requires: #verbose?, #no_harm?
+module FileOperations
+
+ def mkdir_p(dirname, prefix = nil)
+ dirname = prefix + File.expand_path(dirname) if prefix
+ $stderr.puts "mkdir -p #{dirname}" if verbose?
+ return if no_harm?
+
+ # Does not check '/', it's too abnormal.
+ dirs = File.expand_path(dirname).split(%r<(?=/)>)
+ if /\A[a-z]:\z/i =~ dirs[0]
+ disk = dirs.shift
+ dirs[0] = disk + dirs[0]
+ end
+ dirs.each_index do |idx|
+ path = dirs[0..idx].join('')
+ Dir.mkdir path unless File.dir?(path)
+ end
+ end
+
+ def rm_f(path)
+ $stderr.puts "rm -f #{path}" if verbose?
+ return if no_harm?
+ force_remove_file path
+ end
+
+ def rm_rf(path)
+ $stderr.puts "rm -rf #{path}" if verbose?
+ return if no_harm?
+ remove_tree path
+ end
+
+ def remove_tree(path)
+ if File.symlink?(path)
+ remove_file path
+ elsif File.dir?(path)
+ remove_tree0 path
+ else
+ force_remove_file path
+ end
+ end
+
+ def remove_tree0(path)
+ Dir.foreach(path) do |ent|
+ next if ent == '.'
+ next if ent == '..'
+ entpath = "#{path}/#{ent}"
+ if File.symlink?(entpath)
+ remove_file entpath
+ elsif File.dir?(entpath)
+ remove_tree0 entpath
+ else
+ force_remove_file entpath
+ end
+ end
+ begin
+ Dir.rmdir path
+ rescue Errno::ENOTEMPTY
+ # directory may not be empty
+ end
+ end
+
+ def move_file(src, dest)
+ force_remove_file dest
+ begin
+ File.rename src, dest
+ rescue
+ File.open(dest, 'wb') {|f|
+ f.write File.binread(src)
+ }
+ File.chmod File.stat(src).mode, dest
+ File.unlink src
+ end
+ end
+
+ def force_remove_file(path)
+ begin
+ remove_file path
+ rescue
+ end
+ end
+
+ def remove_file(path)
+ File.chmod 0777, path
+ File.unlink path
+ end
+
+ def install(from, dest, mode, prefix = nil)
+ $stderr.puts "install #{from} #{dest}" if verbose?
+ return if no_harm?
+
+ realdest = prefix ? prefix + File.expand_path(dest) : dest
+ realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
+ str = File.binread(from)
+ if diff?(str, realdest)
+ verbose_off {
+ rm_f realdest if File.exist?(realdest)
+ }
+ File.open(realdest, 'wb') {|f|
+ f.write str
+ }
+ File.chmod mode, realdest
+
+ File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
+ if prefix
+ f.puts realdest.sub(prefix, '')
+ else
+ f.puts realdest
+ end
+ }
+ end
+ end
+
+ def diff?(new_content, path)
+ return true unless File.exist?(path)
+ new_content != File.binread(path)
+ end
+
+ def command(*args)
+ $stderr.puts args.join(' ') if verbose?
+ system(*args) or raise RuntimeError,
+ "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
+ end
+
+ def ruby(*args)
+ command config('rubyprog'), *args
+ end
+
+ def make(task = nil)
+ command(*[config('makeprog'), task].compact)
+ end
+
+ def extdir?(dir)
+ File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
+ end
+
+ def files_of(dir)
+ Dir.open(dir) {|d|
+ return d.select {|ent| File.file?("#{dir}/#{ent}") }
+ }
+ end
+
+ DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
+
+ def directories_of(dir)
+ Dir.open(dir) {|d|
+ return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
+ }
+ end
+
+end
+
+
+# This module requires: #srcdir_root, #objdir_root, #relpath
+module HookScriptAPI
+
+ def get_config(key)
+ @config[key]
+ end
+
+ alias config get_config
+
+ # obsolete: use metaconfig to change configuration
+ def set_config(key, val)
+ @config[key] = val
+ end
+
+ #
+ # srcdir/objdir (works only in the package directory)
+ #
+
+ def curr_srcdir
+ "#{srcdir_root()}/#{relpath()}"
+ end
+
+ def curr_objdir
+ "#{objdir_root()}/#{relpath()}"
+ end
+
+ def srcfile(path)
+ "#{curr_srcdir()}/#{path}"
+ end
+
+ def srcexist?(path)
+ File.exist?(srcfile(path))
+ end
+
+ def srcdirectory?(path)
+ File.dir?(srcfile(path))
+ end
+
+ def srcfile?(path)
+ File.file?(srcfile(path))
+ end
+
+ def srcentries(path = '.')
+ Dir.open("#{curr_srcdir()}/#{path}") {|d|
+ return d.to_a - %w(. ..)
+ }
+ end
+
+ def srcfiles(path = '.')
+ srcentries(path).select {|fname|
+ File.file?(File.join(curr_srcdir(), path, fname))
+ }
+ end
+
+ def srcdirectories(path = '.')
+ srcentries(path).select {|fname|
+ File.dir?(File.join(curr_srcdir(), path, fname))
+ }
+ end
+
+end
+
+
+class ToplevelInstaller
+
+ Version = '3.4.1'
+ Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
+
+ TASKS = [
+ [ 'all', 'do config, setup, then install' ],
+ [ 'config', 'saves your configurations' ],
+ [ 'show', 'shows current configuration' ],
+ [ 'setup', 'compiles ruby extentions and others' ],
+ [ 'install', 'installs files' ],
+ [ 'test', 'run all tests in test/' ],
+ [ 'clean', "does `make clean' for each extention" ],
+ [ 'distclean',"does `make distclean' for each extention" ]
+ ]
+
+ def ToplevelInstaller.invoke
+ config = ConfigTable.new(load_rbconfig())
+ config.load_standard_entries
+ config.load_multipackage_entries if multipackage?
+ config.fixup
+ klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
+ klass.new(File.dirname($0), config).invoke
+ end
+
+ def ToplevelInstaller.multipackage?
+ File.dir?(File.dirname($0) + '/packages')
+ end
+
+ def ToplevelInstaller.load_rbconfig
+ if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
+ ARGV.delete(arg)
+ load File.expand_path(arg.split(/=/, 2)[1])
+ $".push 'rbconfig.rb'
+ else
+ require 'rbconfig'
+ end
+ ::Config::CONFIG
+ end
+
+ def initialize(ardir_root, config)
+ @ardir = File.expand_path(ardir_root)
+ @config = config
+ # cache
+ @valid_task_re = nil
+ end
+
+ def config(key)
+ @config[key]
+ end
+
+ def inspect
+ "#<#{self.class} #{__id__()}>"
+ end
+
+ def invoke
+ run_metaconfigs
+ case task = parsearg_global()
+ when nil, 'all'
+ parsearg_config
+ init_installers
+ exec_config
+ exec_setup
+ exec_install
+ else
+ case task
+ when 'config', 'test'
+ ;
+ when 'clean', 'distclean'
+ @config.load_savefile if File.exist?(@config.savefile)
+ else
+ @config.load_savefile
+ end
+ __send__ "parsearg_#{task}"
+ init_installers
+ __send__ "exec_#{task}"
+ end
+ end
+
+ def run_metaconfigs
+ @config.load_script "#{@ardir}/metaconfig"
+ end
+
+ def init_installers
+ @installer = Installer.new(@config, @ardir, File.expand_path('.'))
+ end
+
+ #
+ # Hook Script API bases
+ #
+
+ def srcdir_root
+ @ardir
+ end
+
+ def objdir_root
+ '.'
+ end
+
+ def relpath
+ '.'
+ end
+
+ #
+ # Option Parsing
+ #
+
+ def parsearg_global
+ while arg = ARGV.shift
+ case arg
+ when /\A\w+\z/
+ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
+ return arg
+ when '-q', '--quiet'
+ @config.verbose = false
+ when '--verbose'
+ @config.verbose = true
+ when '--help'
+ print_usage $stdout
+ exit 0
+ when '--version'
+ puts "#{File.basename($0)} version #{Version}"
+ exit 0
+ when '--copyright'
+ puts Copyright
+ exit 0
+ else
+ setup_rb_error "unknown global option '#{arg}'"
+ end
+ end
+ nil
+ end
+
+ def valid_task?(t)
+ valid_task_re() =~ t
+ end
+
+ def valid_task_re
+ @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
+ end
+
+ def parsearg_no_options
+ unless ARGV.empty?
+ task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
+ setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
+ end
+ end
+
+ alias parsearg_show parsearg_no_options
+ alias parsearg_setup parsearg_no_options
+ alias parsearg_test parsearg_no_options
+ alias parsearg_clean parsearg_no_options
+ alias parsearg_distclean parsearg_no_options
+
+ def parsearg_config
+ evalopt = []
+ set = []
+ @config.config_opt = []
+ while i = ARGV.shift
+ if /\A--?\z/ =~ i
+ @config.config_opt = ARGV.dup
+ break
+ end
+ name, value = *@config.parse_opt(i)
+ if @config.value_config?(name)
+ @config[name] = value
+ else
+ evalopt.push [name, value]
+ end
+ set.push name
+ end
+ evalopt.each do |name, value|
+ @config.lookup(name).evaluate value, @config
+ end
+ # Check if configuration is valid
+ set.each do |n|
+ @config[n] if @config.value_config?(n)
+ end
+ end
+
+ def parsearg_install
+ @config.no_harm = false
+ @config.install_prefix = ''
+ while a = ARGV.shift
+ case a
+ when '--no-harm'
+ @config.no_harm = true
+ when /\A--prefix=/
+ path = a.split(/=/, 2)[1]
+ path = File.expand_path(path) unless path[0,1] == '/'
+ @config.install_prefix = path
+ else
+ setup_rb_error "install: unknown option #{a}"
+ end
+ end
+ end
+
+ def print_usage(out)
+ out.puts 'Typical Installation Procedure:'
+ out.puts " $ ruby #{File.basename $0} config"
+ out.puts " $ ruby #{File.basename $0} setup"
+ out.puts " # ruby #{File.basename $0} install (may require root privilege)"
+ out.puts
+ out.puts 'Detailed Usage:'
+ out.puts " ruby #{File.basename $0} <global option>"
+ out.puts " ruby #{File.basename $0} [<global options>] <task> [<task options>]"
+
+ fmt = " %-24s %s\n"
+ out.puts
+ out.puts 'Global options:'
+ out.printf fmt, '-q,--quiet', 'suppress message outputs'
+ out.printf fmt, ' --verbose', 'output messages verbosely'
+ out.printf fmt, ' --help', 'print this message'
+ out.printf fmt, ' --version', 'print version and quit'
+ out.printf fmt, ' --copyright', 'print copyright and quit'
+ out.puts
+ out.puts 'Tasks:'
+ TASKS.each do |name, desc|
+ out.printf fmt, name, desc
+ end
+
+ fmt = " %-24s %s [%s]\n"
+ out.puts
+ out.puts 'Options for CONFIG or ALL:'
+ @config.each do |item|
+ out.printf fmt, item.help_opt, item.description, item.help_default
+ end
+ out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
+ out.puts
+ out.puts 'Options for INSTALL:'
+ out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
+ out.printf fmt, '--prefix=path', 'install path prefix', ''
+ out.puts
+ end
+
+ #
+ # Task Handlers
+ #
+
+ def exec_config
+ @installer.exec_config
+ @config.save # must be final
+ end
+
+ def exec_setup
+ @installer.exec_setup
+ end
+
+ def exec_install
+ @installer.exec_install
+ end
+
+ def exec_test
+ @installer.exec_test
+ end
+
+ def exec_show
+ @config.each do |i|
+ printf "%-20s %s\n", i.name, i.value if i.value?
+ end
+ end
+
+ def exec_clean
+ @installer.exec_clean
+ end
+
+ def exec_distclean
+ @installer.exec_distclean
+ end
+
+end # class ToplevelInstaller
+
+
+class ToplevelInstallerMulti < ToplevelInstaller
+
+ include FileOperations
+
+ def initialize(ardir_root, config)
+ super
+ @packages = directories_of("#{@ardir}/packages")
+ raise 'no package exists' if @packages.empty?
+ @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
+ end
+
+ def run_metaconfigs
+ @config.load_script "#{@ardir}/metaconfig", self
+ @packages.each do |name|
+ @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
+ end
+ end
+
+ attr_reader :packages
+
+ def packages=(list)
+ raise 'package list is empty' if list.empty?
+ list.each do |name|
+ raise "directory packages/#{name} does not exist"\
+ unless File.dir?("#{@ardir}/packages/#{name}")
+ end
+ @packages = list
+ end
+
+ def init_installers
+ @installers = {}
+ @packages.each do |pack|
+ @installers[pack] = Installer.new(@config,
+ "#{@ardir}/packages/#{pack}",
+ "packages/#{pack}")
+ end
+ with = extract_selection(config('with'))
+ without = extract_selection(config('without'))
+ @selected = @installers.keys.select {|name|
+ (with.empty? or with.include?(name)) \
+ and not without.include?(name)
+ }
+ end
+
+ def extract_selection(list)
+ a = list.split(/,/)
+ a.each do |name|
+ setup_rb_error "no such package: #{name}" unless @installers.key?(name)
+ end
+ a
+ end
+
+ def print_usage(f)
+ super
+ f.puts 'Inluded packages:'
+ f.puts ' ' + @packages.sort.join(' ')
+ f.puts
+ end
+
+ #
+ # Task Handlers
+ #
+
+ def exec_config
+ run_hook 'pre-config'
+ each_selected_installers {|inst| inst.exec_config }
+ run_hook 'post-config'
+ @config.save # must be final
+ end
+
+ def exec_setup
+ run_hook 'pre-setup'
+ each_selected_installers {|inst| inst.exec_setup }
+ run_hook 'post-setup'
+ end
+
+ def exec_install
+ run_hook 'pre-install'
+ each_selected_installers {|inst| inst.exec_install }
+ run_hook 'post-install'
+ end
+
+ def exec_test
+ run_hook 'pre-test'
+ each_selected_installers {|inst| inst.exec_test }
+ run_hook 'post-test'
+ end
+
+ def exec_clean
+ rm_f @config.savefile
+ run_hook 'pre-clean'
+ each_selected_installers {|inst| inst.exec_clean }
+ run_hook 'post-clean'
+ end
+
+ def exec_distclean
+ rm_f @config.savefile
+ run_hook 'pre-distclean'
+ each_selected_installers {|inst| inst.exec_distclean }
+ run_hook 'post-distclean'
+ end
+
+ #
+ # lib
+ #
+
+ def each_selected_installers
+ Dir.mkdir 'packages' unless File.dir?('packages')
+ @selected.each do |pack|
+ $stderr.puts "Processing the package `#{pack}' ..." if verbose?
+ Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
+ Dir.chdir "packages/#{pack}"
+ yield @installers[pack]
+ Dir.chdir '../..'
+ end
+ end
+
+ def run_hook(id)
+ @root_installer.run_hook id
+ end
+
+ # module FileOperations requires this
+ def verbose?
+ @config.verbose?
+ end
+
+ # module FileOperations requires this
+ def no_harm?
+ @config.no_harm?
+ end
+
+end # class ToplevelInstallerMulti
+
+
+class Installer
+
+ FILETYPES = %w( bin lib ext data conf man )
+
+ include FileOperations
+ include HookScriptAPI
+
+ def initialize(config, srcroot, objroot)
+ @config = config
+ @srcdir = File.expand_path(srcroot)
+ @objdir = File.expand_path(objroot)
+ @currdir = '.'
+ end
+
+ def inspect
+ "#<#{self.class} #{File.basename(@srcdir)}>"
+ end
+
+ def noop(rel)
+ end
+
+ #
+ # Hook Script API base methods
+ #
+
+ def srcdir_root
+ @srcdir
+ end
+
+ def objdir_root
+ @objdir
+ end
+
+ def relpath
+ @currdir
+ end
+
+ #
+ # Config Access
+ #
+
+ # module FileOperations requires this
+ def verbose?
+ @config.verbose?
+ end
+
+ # module FileOperations requires this
+ def no_harm?
+ @config.no_harm?
+ end
+
+ def verbose_off
+ begin
+ save, @config.verbose = @config.verbose?, false
+ yield
+ ensure
+ @config.verbose = save
+ end
+ end
+
+ #
+ # TASK config
+ #
+
+ def exec_config
+ exec_task_traverse 'config'
+ end
+
+ alias config_dir_bin noop
+ alias config_dir_lib noop
+
+ def config_dir_ext(rel)
+ extconf if extdir?(curr_srcdir())
+ end
+
+ alias config_dir_data noop
+ alias config_dir_conf noop
+ alias config_dir_man noop
+
+ def extconf
+ ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
+ end
+
+ #
+ # TASK setup
+ #
+
+ def exec_setup
+ exec_task_traverse 'setup'
+ end
+
+ def setup_dir_bin(rel)
+ files_of(curr_srcdir()).each do |fname|
+ update_shebang_line "#{curr_srcdir()}/#{fname}"
+ end
+ end
+
+ alias setup_dir_lib noop
+
+ def setup_dir_ext(rel)
+ make if extdir?(curr_srcdir())
+ end
+
+ alias setup_dir_data noop
+ alias setup_dir_conf noop
+ alias setup_dir_man noop
+
+ def update_shebang_line(path)
+ return if no_harm?
+ return if config('shebang') == 'never'
+ old = Shebang.load(path)
+ if old
+ $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1
+ new = new_shebang(old)
+ return if new.to_s == old.to_s
+ else
+ return unless config('shebang') == 'all'
+ new = Shebang.new(config('rubypath'))
+ end
+ $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
+ open_atomic_writer(path) {|output|
+ File.open(path, 'rb') {|f|
+ f.gets if old # discard
+ output.puts new.to_s
+ output.print f.read
+ }
+ }
+ end
+
+ def new_shebang(old)
+ if /\Aruby/ =~ File.basename(old.cmd)
+ Shebang.new(config('rubypath'), old.args)
+ elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
+ Shebang.new(config('rubypath'), old.args[1..-1])
+ else
+ return old unless config('shebang') == 'all'
+ Shebang.new(config('rubypath'))
+ end
+ end
+
+ def open_atomic_writer(path, &block)
+ tmpfile = File.basename(path) + '.tmp'
+ begin
+ File.open(tmpfile, 'wb', &block)
+ File.rename tmpfile, File.basename(path)
+ ensure
+ File.unlink tmpfile if File.exist?(tmpfile)
+ end
+ end
+
+ class Shebang
+ def Shebang.load(path)
+ line = nil
+ File.open(path) {|f|
+ line = f.gets
+ }
+ return nil unless /\A#!/ =~ line
+ parse(line)
+ end
+
+ def Shebang.parse(line)
+ cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
+ new(cmd, args)
+ end
+
+ def initialize(cmd, args = [])
+ @cmd = cmd
+ @args = args
+ end
+
+ attr_reader :cmd
+ attr_reader :args
+
+ def to_s
+ "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
+ end
+ end
+
+ #
+ # TASK install
+ #
+
+ def exec_install
+ rm_f 'InstalledFiles'
+ exec_task_traverse 'install'
+ end
+
+ def install_dir_bin(rel)
+ install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
+ end
+
+ def install_dir_lib(rel)
+ install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
+ end
+
+ def install_dir_ext(rel)
+ return unless extdir?(curr_srcdir())
+ install_files rubyextentions('.'),
+ "#{config('sodir')}/#{File.dirname(rel)}",
+ 0555
+ end
+
+ def install_dir_data(rel)
+ install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
+ end
+
+ def install_dir_conf(rel)
+ # FIXME: should not remove current config files
+ # (rename previous file to .old/.org)
+ install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
+ end
+
+ def install_dir_man(rel)
+ install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
+ end
+
+ def install_files(list, dest, mode)
+ mkdir_p dest, @config.install_prefix
+ list.each do |fname|
+ install fname, dest, mode, @config.install_prefix
+ end
+ end
+
+ def libfiles
+ glob_reject(%w(*.y *.output), targetfiles())
+ end
+
+ def rubyextentions(dir)
+ ents = glob_select("*.#{@config.dllext}", targetfiles())
+ if ents.empty?
+ setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
+ end
+ ents
+ end
+
+ def targetfiles
+ mapdir(existfiles() - hookfiles())
+ end
+
+ def mapdir(ents)
+ ents.map {|ent|
+ if File.exist?(ent)
+ then ent # objdir
+ else "#{curr_srcdir()}/#{ent}" # srcdir
+ end
+ }
+ end
+
+ # picked up many entries from cvs-1.11.1/src/ignore.c
+ JUNK_FILES = %w(
+ core RCSLOG tags TAGS .make.state
+ .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
+ *~ *.old *.bak *.BAK *.orig *.rej _$* *$
+
+ *.org *.in .*
+ )
+
+ def existfiles
+ glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
+ end
+
+ def hookfiles
+ %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
+ %w( config setup install clean ).map {|t| sprintf(fmt, t) }
+ }.flatten
+ end
+
+ def glob_select(pat, ents)
+ re = globs2re([pat])
+ ents.select {|ent| re =~ ent }
+ end
+
+ def glob_reject(pats, ents)
+ re = globs2re(pats)
+ ents.reject {|ent| re =~ ent }
+ end
+
+ GLOB2REGEX = {
+ '.' => '\.',
+ '$' => '\$',
+ '#' => '\#',
+ '*' => '.*'
+ }
+
+ def globs2re(pats)
+ /\A(?:#{
+ pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
+ })\z/
+ end
+
+ #
+ # TASK test
+ #
+
+ TESTDIR = 'test'
+
+ def exec_test
+ unless File.directory?('test')
+ $stderr.puts 'no test in this package' if verbose?
+ return
+ end
+ $stderr.puts 'Running tests...' if verbose?
+ begin
+ require 'test/unit'
+ rescue LoadError
+ setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.'
+ end
+ runner = Test::Unit::AutoRunner.new(true)
+ runner.to_run << TESTDIR
+ runner.run
+ end
+
+ #
+ # TASK clean
+ #
+
+ def exec_clean
+ exec_task_traverse 'clean'
+ rm_f @config.savefile
+ rm_f 'InstalledFiles'
+ end
+
+ alias clean_dir_bin noop
+ alias clean_dir_lib noop
+ alias clean_dir_data noop
+ alias clean_dir_conf noop
+ alias clean_dir_man noop
+
+ def clean_dir_ext(rel)
+ return unless extdir?(curr_srcdir())
+ make 'clean' if File.file?('Makefile')
+ end
+
+ #
+ # TASK distclean
+ #
+
+ def exec_distclean
+ exec_task_traverse 'distclean'
+ rm_f @config.savefile
+ rm_f 'InstalledFiles'
+ end
+
+ alias distclean_dir_bin noop
+ alias distclean_dir_lib noop
+
+ def distclean_dir_ext(rel)
+ return unless extdir?(curr_srcdir())
+ make 'distclean' if File.file?('Makefile')
+ end
+
+ alias distclean_dir_data noop
+ alias distclean_dir_conf noop
+ alias distclean_dir_man noop
+
+ #
+ # Traversing
+ #
+
+ def exec_task_traverse(task)
+ run_hook "pre-#{task}"
+ FILETYPES.each do |type|
+ if type == 'ext' and config('without-ext') == 'yes'
+ $stderr.puts 'skipping ext/* by user option' if verbose?
+ next
+ end
+ traverse task, type, "#{task}_dir_#{type}"
+ end
+ run_hook "post-#{task}"
+ end
+
+ def traverse(task, rel, mid)
+ dive_into(rel) {
+ run_hook "pre-#{task}"
+ __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
+ directories_of(curr_srcdir()).each do |d|
+ traverse task, "#{rel}/#{d}", mid
+ end
+ run_hook "post-#{task}"
+ }
+ end
+
+ def dive_into(rel)
+ return unless File.dir?("#{@srcdir}/#{rel}")
+
+ dir = File.basename(rel)
+ Dir.mkdir dir unless File.dir?(dir)
+ prevdir = Dir.pwd
+ Dir.chdir dir
+ $stderr.puts '---> ' + rel if verbose?
+ @currdir = rel
+ yield
+ Dir.chdir prevdir
+ $stderr.puts '<--- ' + rel if verbose?
+ @currdir = File.dirname(rel)
+ end
+
+ def run_hook(id)
+ path = [ "#{curr_srcdir()}/#{id}",
+ "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) }
+ return unless path
+ begin
+ instance_eval File.read(path), path, 1
+ rescue
+ raise if $DEBUG
+ setup_rb_error "hook #{path} failed:\n" + $!.message
+ end
+ end
+
+end # class Installer
+
+
+class SetupError < StandardError; end
+
+def setup_rb_error(msg)
+ raise SetupError, msg
+end
+
+if $0 == __FILE__
+ begin
+ ToplevelInstaller.invoke
+ rescue SetupError
+ raise if $DEBUG
+ $stderr.puts $!.message
+ $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
+ exit 1
+ end
+end
--- /dev/null
+#!/usr/bin/env ruby
+
+# test_binarytree.rb
+#
+# $Revision: 1.5 $ by $Author: anupamsg $
+# $Name: $
+#
+# Copyright (c) 2006, 2007 Anupam Sengupta
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# - Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+#
+# - Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+require 'test/unit'
+require 'tree/binarytree'
+
+module TestTree
+ # Test class for the Tree node.
+ class TestBinaryTreeNode < Test::Unit::TestCase
+
+ def setup
+ @root = Tree::BinaryTreeNode.new("ROOT", "Root Node")
+
+ @left_child1 = Tree::BinaryTreeNode.new("A Child at Left", "Child Node @ left")
+ @right_child1 = Tree::BinaryTreeNode.new("B Child at Right", "Child Node @ right")
+
+ end
+
+ def teardown
+ @root.remove!(@left_child1)
+ @root.remove!(@right_child1)
+ @root = nil
+ end
+
+ def test_initialize
+ assert_not_nil(@root, "Binary tree's Root should have been created")
+ end
+
+ def test_add
+ @root.add @left_child1
+ assert_same(@left_child1, @root.leftChild, "The left node should be left_child1")
+ assert_same(@left_child1, @root.firstChild, "The first node should be left_child1")
+
+ @root.add @right_child1
+ assert_same(@right_child1, @root.rightChild, "The right node should be right_child1")
+ assert_same(@right_child1, @root.lastChild, "The first node should be right_child1")
+
+ assert_raise RuntimeError do
+ @root.add Tree::BinaryTreeNode.new("The third child!")
+ end
+
+ assert_raise RuntimeError do
+ @root << Tree::BinaryTreeNode.new("The third child!")
+ end
+ end
+
+ def test_leftChild
+ @root << @left_child1
+ @root << @right_child1
+ assert_same(@left_child1, @root.leftChild, "The left child should be 'left_child1")
+ assert_not_same(@right_child1, @root.leftChild, "The right_child1 is not the left child")
+ end
+
+ def test_rightChild
+ @root << @left_child1
+ @root << @right_child1
+ assert_same(@right_child1, @root.rightChild, "The right child should be 'right_child1")
+ assert_not_same(@left_child1, @root.rightChild, "The left_child1 is not the left child")
+ end
+
+ def test_leftChild_equals
+ @root << @left_child1
+ @root << @right_child1
+ assert_same(@left_child1, @root.leftChild, "The left child should be 'left_child1")
+
+ @root.leftChild = Tree::BinaryTreeNode.new("New Left Child")
+ assert_equal("New Left Child", @root.leftChild.name, "The left child should now be the new child")
+ assert_equal("B Child at Right", @root.lastChild.name, "The last child should now be the right child")
+
+ # Now set the left child as nil, and retest
+ @root.leftChild = nil
+ assert_nil(@root.leftChild, "The left child should now be nil")
+ assert_nil(@root.firstChild, "The first child is now nil")
+ assert_equal("B Child at Right", @root.lastChild.name, "The last child should now be the right child")
+ end
+
+ def test_rightChild_equals
+ @root << @left_child1
+ @root << @right_child1
+ assert_same(@right_child1, @root.rightChild, "The right child should be 'right_child1")
+
+ @root.rightChild = Tree::BinaryTreeNode.new("New Right Child")
+ assert_equal("New Right Child", @root.rightChild.name, "The right child should now be the new child")
+ assert_equal("A Child at Left", @root.firstChild.name, "The first child should now be the left child")
+ assert_equal("New Right Child", @root.lastChild.name, "The last child should now be the right child")
+
+ # Now set the right child as nil, and retest
+ @root.rightChild = nil
+ assert_nil(@root.rightChild, "The right child should now be nil")
+ assert_equal("A Child at Left", @root.firstChild.name, "The first child should now be the left child")
+ assert_nil(@root.lastChild, "The first child is now nil")
+ end
+
+ def test_isLeftChild_eh
+ @root << @left_child1
+ @root << @right_child1
+
+ assert(@left_child1.isLeftChild?, "left_child1 should be the left child")
+ assert(!@right_child1.isLeftChild?, "left_child1 should be the left child")
+
+ # Now set the right child as nil, and retest
+ @root.rightChild = nil
+ assert(@left_child1.isLeftChild?, "left_child1 should be the left child")
+
+ assert(!@root.isLeftChild?, "Root is neither left child nor right")
+ end
+
+ def test_isRightChild_eh
+ @root << @left_child1
+ @root << @right_child1
+
+ assert(@right_child1.isRightChild?, "right_child1 should be the right child")
+ assert(!@left_child1.isRightChild?, "right_child1 should be the right child")
+
+ # Now set the left child as nil, and retest
+ @root.leftChild = nil
+ assert(@right_child1.isRightChild?, "right_child1 should be the right child")
+ assert(!@root.isRightChild?, "Root is neither left child nor right")
+ end
+
+ def test_swap_children
+ @root << @left_child1
+ @root << @right_child1
+
+ assert(@right_child1.isRightChild?, "right_child1 should be the right child")
+ assert(!@left_child1.isRightChild?, "right_child1 should be the right child")
+
+ @root.swap_children
+
+ assert(@right_child1.isLeftChild?, "right_child1 should now be the left child")
+ assert(@left_child1.isRightChild?, "left_child1 should now be the right child")
+ assert_equal(@right_child1, @root.firstChild, "right_child1 should now be the first child")
+ assert_equal(@left_child1, @root.lastChild, "left_child1 should now be the last child")
+ assert_equal(@right_child1, @root[0], "right_child1 should now be the first child")
+ assert_equal(@left_child1, @root[1], "left_child1 should now be the last child")
+ end
+ end
+end
+
+# $Log: test_binarytree.rb,v $
+# Revision 1.5 2007/12/22 00:28:59 anupamsg
+# Added more test cases, and enabled ZenTest compatibility.
+#
+# Revision 1.4 2007/12/18 23:11:29 anupamsg
+# Minor documentation changes in the binarytree class.
+#
+# Revision 1.3 2007/10/02 03:07:30 anupamsg
+# * Rakefile: Added an optional task for rcov code coverage.
+#
+# * test/test_binarytree.rb: Removed the unnecessary dependency on "Person" class.
+#
+# * test/test_tree.rb: Removed dependency on the redundant "Person" class.
+#
+# Revision 1.2 2007/08/30 22:06:13 anupamsg
+# Added a new swap_children method for the Binary Tree class.
+# Also made minor documentation updates and test additions.
+#
+# Revision 1.1 2007/07/21 04:52:37 anupamsg
+# Renamed the test files.
+#
+# Revision 1.4 2007/07/19 02:03:57 anupamsg
+# Minor syntax correction.
+#
+# Revision 1.3 2007/07/19 02:02:12 anupamsg
+# Removed useless files (including rdoc, which should be generated for each release.
+#
+# Revision 1.2 2007/07/18 20:15:06 anupamsg
+# Added two predicate methods in BinaryTreeNode to determine whether a node
+# is a left or a right node.
+#
--- /dev/null
+#!/usr/bin/env ruby
+
+# testtree.rb
+#
+# $Revision: 1.6 $ by $Author: anupamsg $
+# $Name: $
+#
+# Copyright (c) 2006, 2007 Anupam Sengupta
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# - Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or
+# other materials provided with the distribution.
+#
+# - Neither the name of the organization nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+require 'test/unit'
+require 'tree'
+
+module TestTree
+ # Test class for the Tree node.
+ class TestTreeNode < Test::Unit::TestCase
+
+ Person = Struct::new(:First, :last)
+
+ def setup
+ @root = Tree::TreeNode.new("ROOT", "Root Node")
+
+ @child1 = Tree::TreeNode.new("Child1", "Child Node 1")
+ @child2 = Tree::TreeNode.new("Child2", "Child Node 2")
+ @child3 = Tree::TreeNode.new("Child3", "Child Node 3")
+ @child4 = Tree::TreeNode.new("Child31", "Grand Child 1")
+
+ end
+
+ # Create this structure for the tests
+ #
+ # +----------+
+ # | ROOT |
+ # +-+--------+
+ # |
+ # | +---------------+
+ # +----+ CHILD1 |
+ # | +---------------+
+ # |
+ # | +---------------+
+ # +----+ CHILD2 |
+ # | +---------------+
+ # |
+ # | +---------------+ +------------------+
+ # +----+ CHILD3 +---+ CHILD4 |
+ # +---------------+ +------------------+
+ #
+ def loadChildren
+ @root << @child1
+ @root << @child2
+ @root << @child3 << @child4
+ end
+
+ def teardown
+ @root = nil
+ end
+
+ def test_root_setup
+ assert_not_nil(@root, "Root cannot be nil")
+ assert_nil(@root.parent, "Parent of root node should be nil")
+ assert_not_nil(@root.name, "Name should not be nil")
+ assert_equal("ROOT", @root.name, "Name should be 'ROOT'")
+ assert_equal("Root Node", @root.content, "Content should be 'Root Node'")
+ assert(@root.isRoot?, "Should identify as root")
+ assert(!@root.hasChildren?, "Cannot have any children")
+ assert(@root.hasContent?, "This root should have content")
+ assert_equal(1, @root.size, "Number of nodes should be one")
+ assert_nil(@root.siblings, "Root cannot have any children")
+
+ assert_raise(RuntimeError) { Tree::TreeNode.new(nil) }
+ end
+
+ def test_root
+ loadChildren
+
+ assert_same(@root, @root.root, "Root's root is self")
+ assert_same(@root, @child1.root, "Root should be ROOT")
+ assert_same(@root, @child4.root, "Root should be ROOT")
+ end
+
+ def test_hasContent_eh
+ aNode = Tree::TreeNode.new("A Node")
+ assert_nil(aNode.content, "The node should not have content")
+ assert(!aNode.hasContent?, "The node should not have content")
+
+ aNode.content = "Something"
+ assert_not_nil(aNode.content, "The node should now have content")
+ assert(aNode.hasContent?, "The node should now have content")
+ end
+
+ def test_length
+ loadChildren
+ assert_equal(@root.size, @root.length, "Length and size methods should return the same result")
+ end
+
+ def test_spaceship # Test the <=> operator.
+ firstNode = Tree::TreeNode.new(1)
+ secondNode = Tree::TreeNode.new(2)
+
+ assert_equal(firstNode <=> nil, +1)
+ assert_equal(firstNode <=> secondNode, -1)
+
+ secondNode = Tree::TreeNode.new(1)
+ assert_equal(firstNode <=> secondNode, 0)
+
+ firstNode = Tree::TreeNode.new("ABC")
+ secondNode = Tree::TreeNode.new("XYZ")
+
+ assert_equal(firstNode <=> nil, +1)
+ assert_equal(firstNode <=> secondNode, -1)
+
+ secondNode = Tree::TreeNode.new("ABC")
+ assert_equal(firstNode <=> secondNode, 0)
+ end
+
+ def test_to_s
+ aNode = Tree::TreeNode.new("A Node", "Some Content")
+
+ expectedString = "Node Name: A Node Content: Some Content Parent: <None> Children: 0 Total Nodes: 1"
+
+ assert_equal(expectedString, aNode.to_s, "The string representation should be same")
+ end
+
+ def test_firstSibling
+ loadChildren
+
+ assert_same(@root, @root.firstSibling, "Root's first sibling is itself")
+ assert_same(@child1, @child1.firstSibling, "Child1's first sibling is itself")
+ assert_same(@child1, @child2.firstSibling, "Child2's first sibling should be child1")
+ assert_same(@child1, @child3.firstSibling, "Child3's first sibling should be child1")
+ assert_not_same(@child1, @child4.firstSibling, "Child4's first sibling is itself")
+ end
+
+ def test_isFirstSibling_eh
+ loadChildren
+
+ assert(@root.isFirstSibling?, "Root's first sibling is itself")
+ assert( @child1.isFirstSibling?, "Child1's first sibling is itself")
+ assert(!@child2.isFirstSibling?, "Child2 is not the first sibling")
+ assert(!@child3.isFirstSibling?, "Child3 is not the first sibling")
+ assert( @child4.isFirstSibling?, "Child4's first sibling is itself")
+ end
+
+ def test_isLastSibling_eh
+ loadChildren
+
+ assert(@root.isLastSibling?, "Root's last sibling is itself")
+ assert(!@child1.isLastSibling?, "Child1 is not the last sibling")
+ assert(!@child2.isLastSibling?, "Child2 is not the last sibling")
+ assert( @child3.isLastSibling?, "Child3's last sibling is itself")
+ assert( @child4.isLastSibling?, "Child4's last sibling is itself")
+ end
+
+ def test_lastSibling
+ loadChildren
+
+ assert_same(@root, @root.lastSibling, "Root's last sibling is itself")
+ assert_same(@child3, @child1.lastSibling, "Child1's last sibling should be child3")
+ assert_same(@child3, @child2.lastSibling, "Child2's last sibling should be child3")
+ assert_same(@child3, @child3.lastSibling, "Child3's last sibling should be itself")
+ assert_not_same(@child3, @child4.lastSibling, "Child4's last sibling is itself")
+ end
+
+ def test_siblings
+ loadChildren
+
+ siblings = []
+ @child1.siblings { |sibling| siblings << sibling}
+ assert_equal(2, siblings.length, "Should have two siblings")
+ assert(siblings.include?(@child2), "Should have 2nd child as sibling")
+ assert(siblings.include?(@child3), "Should have 3rd child as sibling")
+
+ siblings.clear
+ siblings = @child1.siblings
+ assert_equal(2, siblings.length, "Should have two siblings")
+
+ siblings.clear
+ @child4.siblings {|sibling| siblings << sibling}
+ assert(siblings.empty?, "Should not have any children")
+
+ end
+
+ def test_isOnlyChild_eh
+ loadChildren
+
+ assert(!@child1.isOnlyChild?, "Child1 is not the only child")
+ assert(!@child2.isOnlyChild?, "Child2 is not the only child")
+ assert(!@child3.isOnlyChild?, "Child3 is not the only child")
+ assert( @child4.isOnlyChild?, "Child4 is not the only child")
+ end
+
+ def test_nextSibling
+ loadChildren
+
+ assert_equal(@child2, @child1.nextSibling, "Child1's next sibling is Child2")
+ assert_equal(@child3, @child2.nextSibling, "Child2's next sibling is Child3")
+ assert_nil(@child3.nextSibling, "Child3 does not have a next sibling")
+ assert_nil(@child4.nextSibling, "Child4 does not have a next sibling")
+ end
+
+ def test_previousSibling
+ loadChildren
+
+ assert_nil(@child1.previousSibling, "Child1 does not have previous sibling")
+ assert_equal(@child1, @child2.previousSibling, "Child2's previous sibling is Child1")
+ assert_equal(@child2, @child3.previousSibling, "Child3's previous sibling is Child2")
+ assert_nil(@child4.previousSibling, "Child4 does not have a previous sibling")
+ end
+
+ def test_add
+ assert(!@root.hasChildren?, "Should not have any children")
+
+ @root.add(@child1)
+
+ @root << @child2
+
+ assert(@root.hasChildren?, "Should have children")
+ assert_equal(3, @root.size, "Should have three nodes")
+
+ @root << @child3 << @child4
+
+ assert_equal(5, @root.size, "Should have five nodes")
+ assert_equal(2, @child3.size, "Should have two nodes")
+
+ assert_raise(RuntimeError) { @root.add(Tree::TreeNode.new(@child1.name)) }
+
+ end
+
+ def test_remove_bang
+ @root << @child1
+ @root << @child2
+
+ assert(@root.hasChildren?, "Should have children")
+ assert_equal(3, @root.size, "Should have three nodes")
+
+ @root.remove!(@child1)
+ assert_equal(2, @root.size, "Should have two nodes")
+ @root.remove!(@child2)
+
+ assert(!@root.hasChildren?, "Should have no children")
+ assert_equal(1, @root.size, "Should have one node")
+
+ @root << @child1
+ @root << @child2
+
+ assert(@root.hasChildren?, "Should have children")
+ assert_equal(3, @root.size, "Should have three nodes")
+
+ @root.removeAll!
+
+ assert(!@root.hasChildren?, "Should have no children")
+ assert_equal(1, @root.size, "Should have one node")
+
+ end
+
+ def test_removeAll_bang
+ loadChildren
+ assert(@root.hasChildren?, "Should have children")
+ @root.removeAll!
+
+ assert(!@root.hasChildren?, "Should have no children")
+ assert_equal(1, @root.size, "Should have one node")
+ end
+
+ def test_removeFromParent_bang
+ loadChildren
+ assert(@root.hasChildren?, "Should have children")
+ assert(!@root.isLeaf?, "Root is not a leaf here")
+
+ child1 = @root[0]
+ assert_not_nil(child1, "Child 1 should exist")
+ assert_same(@root, child1.root, "Child 1's root should be ROOT")
+ assert(@root.include?(child1), "root should have child1")
+ child1.removeFromParent!
+ assert_same(child1, child1.root, "Child 1's root should be self")
+ assert(!@root.include?(child1), "root should not have child1")
+
+ child1.removeFromParent!
+ assert_same(child1, child1.root, "Child 1's root should still be self")
+ end
+
+ def test_children
+ loadChildren
+
+ assert(@root.hasChildren?, "Should have children")
+ assert_equal(5, @root.size, "Should have four nodes")
+ assert(@child3.hasChildren?, "Should have children")
+ assert(!@child3.isLeaf?, "Should not be a leaf")
+
+ children = []
+ for child in @root.children
+ children << child
+ end
+
+ assert_equal(3, children.length, "Should have three direct children")
+ assert(!children.include?(@root), "Should not have root")
+ assert(children.include?(@child1), "Should have child 1")
+ assert(children.include?(@child2), "Should have child 2")
+ assert(children.include?(@child3), "Should have child 3")
+ assert(!children.include?(@child4), "Should not have child 4")
+
+ children.clear
+ children = @root.children
+ assert_equal(3, children.length, "Should have three children")
+
+ end
+
+ def test_firstChild
+ loadChildren
+
+ assert_equal(@child1, @root.firstChild, "Root's first child is Child1")
+ assert_nil(@child1.firstChild, "Child1 does not have any children")
+ assert_equal(@child4, @child3.firstChild, "Child3's first child is Child4")
+
+ end
+
+ def test_lastChild
+ loadChildren
+
+ assert_equal(@child3, @root.lastChild, "Root's last child is Child3")
+ assert_nil(@child1.lastChild, "Child1 does not have any children")
+ assert_equal(@child4, @child3.lastChild, "Child3's last child is Child4")
+
+ end
+
+ def test_find
+ loadChildren
+ foundNode = @root.find { |node| node == @child2}
+ assert_same(@child2, foundNode, "The node should be Child 2")
+
+ foundNode = @root.find { |node| node == @child4}
+ assert_same(@child4, foundNode, "The node should be Child 4")
+
+ foundNode = @root.find { |node| node.name == "Child31" }
+ assert_same(@child4, foundNode, "The node should be Child 4")
+ foundNode = @root.find { |node| node.name == "NOT PRESENT" }
+ assert_nil(foundNode, "The node should not be found")
+ end
+
+ def test_parentage
+ loadChildren
+
+ assert_nil(@root.parentage, "Root does not have any parentage")
+ assert_equal([@root], @child1.parentage, "Child1 has Root as its parent")
+ assert_equal([@child3, @root], @child4.parentage, "Child4 has Child3 and Root as ancestors")
+ end
+
+ def test_each
+ loadChildren
+ assert(@root.hasChildren?, "Should have children")
+ assert_equal(5, @root.size, "Should have five nodes")
+ assert(@child3.hasChildren?, "Should have children")
+
+ nodes = []
+ @root.each { |node| nodes << node }
+
+ assert_equal(5, nodes.length, "Should have FIVE NODES")
+ assert(nodes.include?(@root), "Should have root")
+ assert(nodes.include?(@child1), "Should have child 1")
+ assert(nodes.include?(@child2), "Should have child 2")
+ assert(nodes.include?(@child3), "Should have child 3")
+ assert(nodes.include?(@child4), "Should have child 4")
+ end
+
+ def test_each_leaf
+ loadChildren
+
+ nodes = []
+ @root.each_leaf { |node| nodes << node }
+
+ assert_equal(3, nodes.length, "Should have THREE LEAF NODES")
+ assert(!nodes.include?(@root), "Should not have root")
+ assert(nodes.include?(@child1), "Should have child 1")
+ assert(nodes.include?(@child2), "Should have child 2")
+ assert(!nodes.include?(@child3), "Should not have child 3")
+ assert(nodes.include?(@child4), "Should have child 4")
+ end
+
+ def test_parent
+ loadChildren
+ assert_nil(@root.parent, "Root's parent should be nil")
+ assert_equal(@root, @child1.parent, "Parent should be root")
+ assert_equal(@root, @child3.parent, "Parent should be root")
+ assert_equal(@child3, @child4.parent, "Parent should be child3")
+ assert_equal(@root, @child4.parent.parent, "Parent should be root")
+ end
+
+ def test_indexed_access
+ loadChildren
+ assert_equal(@child1, @root[0], "Should be the first child")
+ assert_equal(@child4, @root[2][0], "Should be the grandchild")
+ assert_nil(@root["TEST"], "Should be nil")
+ assert_raise(RuntimeError) { @root[nil] }
+ end
+
+ def test_printTree
+ loadChildren
+ #puts
+ #@root.printTree
+ end
+
+ # Tests the binary dumping mechanism with an Object content node
+ def test_marshal_dump
+ # Setup Test Data
+ test_root = Tree::TreeNode.new("ROOT", "Root Node")
+ test_content = {"KEY1" => "Value1", "KEY2" => "Value2" }
+ test_child = Tree::TreeNode.new("Child", test_content)
+ test_content2 = ["AValue1", "AValue2", "AValue3"]
+ test_grand_child = Tree::TreeNode.new("Grand Child 1", test_content2)
+ test_root << test_child << test_grand_child
+
+ # Perform the test operation
+ data = Marshal.dump(test_root) # Marshal
+ new_root = Marshal.load(data) # And unmarshal
+
+ # Test the root node
+ assert_equal(test_root.name, new_root.name, "Must identify as ROOT")
+ assert_equal(test_root.content, new_root.content, "Must have root's content")
+ assert(new_root.isRoot?, "Must be the ROOT node")
+ assert(new_root.hasChildren?, "Must have a child node")
+
+ # Test the child node
+ new_child = new_root[test_child.name]
+ assert_equal(test_child.name, new_child.name, "Must have child 1")
+ assert(new_child.hasContent?, "Child must have content")
+ assert(new_child.isOnlyChild?, "Child must be the only child")
+
+ new_child_content = new_child.content
+ assert_equal(Hash, new_child_content.class, "Class of child's content should be a hash")
+ assert_equal(test_child.content.size, new_child_content.size, "The content should have same size")
+
+ # Test the grand-child node
+ new_grand_child = new_child[test_grand_child.name]
+ assert_equal(test_grand_child.name, new_grand_child.name, "Must have grand child")
+ assert(new_grand_child.hasContent?, "Grand-child must have content")
+ assert(new_grand_child.isOnlyChild?, "Grand-child must be the only child")
+
+ new_grand_child_content = new_grand_child.content
+ assert_equal(Array, new_grand_child_content.class, "Class of grand-child's content should be an Array")
+ assert_equal(test_grand_child.content.size, new_grand_child_content.size, "The content should have same size")
+ end
+
+ # marshal_load and marshal_dump are symmetric methods
+ # This alias is for satisfying ZenTest
+ alias test_marshal_load test_marshal_dump
+
+ # Test the collect method from the mixed-in Enumerable functionality.
+ def test_collect
+ loadChildren
+ collectArray = @root.collect do |node|
+ node.content = "abc"
+ node
+ end
+ collectArray.each {|node| assert_equal("abc", node.content, "Should be 'abc'")}
+ end
+
+ # Test freezing the tree
+ def test_freezeTree_bang
+ loadChildren
+ @root.content = "ABC"
+ assert_equal("ABC", @root.content, "Content should be 'ABC'")
+ @root.freezeTree!
+ assert_raise(TypeError) {@root.content = "123"}
+ assert_raise(TypeError) {@root[0].content = "123"}
+ end
+
+ # Test whether the content is accesible
+ def test_content
+ pers = Person::new("John", "Doe")
+ @root.content = pers
+ assert_same(pers, @root.content, "Content should be the same")
+ end
+
+ # Test the depth computation algorithm
+ def test_depth
+ assert_equal(1, @root.depth, "A single node's depth is 1")
+
+ @root << @child1
+ assert_equal(2, @root.depth, "This should be of depth 2")
+
+ @root << @child2
+ assert_equal(2, @root.depth, "This should be of depth 2")
+
+ @child2 << @child3
+ assert_equal(3, @root.depth, "This should be of depth 3")
+ assert_equal(2, @child2.depth, "This should be of depth 2")
+
+ @child3 << @child4
+ assert_equal(4, @root.depth, "This should be of depth 4")
+ end
+
+ # Test the breadth computation algorithm
+ def test_breadth
+ assert_equal(1, @root.breadth, "A single node's breadth is 1")
+
+ @root << @child1
+ assert_equal(1, @root.breadth, "This should be of breadth 1")
+
+ @root << @child2
+ assert_equal(2, @child1.breadth, "This should be of breadth 2")
+ assert_equal(2, @child2.breadth, "This should be of breadth 2")
+
+ @root << @child3
+ assert_equal(3, @child1.breadth, "This should be of breadth 3")
+ assert_equal(3, @child2.breadth, "This should be of breadth 3")
+
+ @child3 << @child4
+ assert_equal(1, @child4.breadth, "This should be of breadth 1")
+ end
+
+ # Test the breadth for each
+ def test_breadth_each
+ j = Tree::TreeNode.new("j")
+ f = Tree::TreeNode.new("f")
+ k = Tree::TreeNode.new("k")
+ a = Tree::TreeNode.new("a")
+ d = Tree::TreeNode.new("d")
+ h = Tree::TreeNode.new("h")
+ z = Tree::TreeNode.new("z")
+
+ # The expected order of response
+ expected_array = [j,
+ f, k,
+ a, h, z,
+ d]
+
+ # Create the following Tree
+ # j <-- level 0 (Root)
+ # / \
+ # f k <-- level 1
+ # / \ \
+ # a h z <-- level 2
+ # \
+ # d <-- level 3
+ j << f << a << d
+ f << h
+ j << k << z
+
+ # Create the response
+ result_array = Array.new
+ j.breadth_each { |node| result_array << node.detached_copy }
+
+ expected_array.each_index do |i|
+ assert_equal(expected_array[i].name, result_array[i].name) # Match only the names.
+ end
+ end
+
+
+ def test_preordered_each
+ j = Tree::TreeNode.new("j")
+ f = Tree::TreeNode.new("f")
+ k = Tree::TreeNode.new("k")
+ a = Tree::TreeNode.new("a")
+ d = Tree::TreeNode.new("d")
+ h = Tree::TreeNode.new("h")
+ z = Tree::TreeNode.new("z")
+
+ # The expected order of response
+ expected_array = [j, f, a, d, h, k, z]
+
+ # Create the following Tree
+ # j <-- level 0 (Root)
+ # / \
+ # f k <-- level 1
+ # / \ \
+ # a h z <-- level 2
+ # \
+ # d <-- level 3
+ j << f << a << d
+ f << h
+ j << k << z
+
+ result_array = []
+ j.preordered_each { |node| result_array << node.detached_copy}
+
+ expected_array.each_index do |i|
+ # Match only the names.
+ assert_equal(expected_array[i].name, result_array[i].name)
+ end
+ end
+
+ def test_detached_copy
+ loadChildren
+
+ assert(@root.hasChildren?, "The root should have children")
+ copy_of_root = @root.detached_copy
+ assert(!copy_of_root.hasChildren?, "The copy should not have children")
+ assert_equal(@root.name, copy_of_root.name, "The names should be equal")
+
+ # Try the same test with a child node
+ assert(!@child3.isRoot?, "Child 3 is not a root")
+ assert(@child3.hasChildren?, "Child 3 has children")
+ copy_of_child3 = @child3.detached_copy
+ assert(copy_of_child3.isRoot?, "Child 3's copy is a root")
+ assert(!copy_of_child3.hasChildren?, "Child 3's copy does not have children")
+ end
+
+ def test_hasChildren_eh
+ loadChildren
+ assert(@root.hasChildren?, "The Root node MUST have children")
+ end
+
+ def test_isLeaf_eh
+ loadChildren
+ assert(!@child3.isLeaf?, "Child 3 is not a leaf node")
+ assert(@child4.isLeaf?, "Child 4 is a leaf node")
+ end
+
+ def test_isRoot_eh
+ loadChildren
+ assert(@root.isRoot?, "The ROOT node must respond as the root node")
+ end
+
+ def test_content_equals
+ @root.content = nil
+ assert_nil(@root.content, "Root's content should be nil")
+ @root.content = "ABCD"
+ assert_equal("ABCD", @root.content, "Root's content should now be 'ABCD'")
+ end
+
+ def test_size
+ assert_equal(1, @root.size, "Root's size should be 1")
+ loadChildren
+ assert_equal(5, @root.size, "Root's size should be 5")
+ assert_equal(2, @child3.size, "Child 3's size should be 2")
+ end
+
+ def test_lt2 # Test the << method
+ @root << @child1
+ @root << @child2
+ @root << @child3 << @child4
+ assert_not_nil(@root['Child1'], "Child 1 should have been added to Root")
+ assert_not_nil(@root['Child2'], "Child 2 should have been added to Root")
+ assert_not_nil(@root['Child3'], "Child 3 should have been added to Root")
+ assert_not_nil(@child3['Child31'], "Child 31 should have been added to Child3")
+ end
+
+ def test_index # Test the [] method
+ assert_raise(RuntimeError) {@root[nil]}
+
+ @root << @child1
+ @root << @child2
+ assert_equal(@child1.name, @root['Child1'].name, "Child 1 should be returned")
+ assert_equal(@child1.name, @root[0].name, "Child 1 should be returned")
+ assert_equal(@child2.name, @root['Child2'].name, "Child 2 should be returned")
+ assert_equal(@child2.name, @root[1].name, "Child 2 should be returned")
+
+ assert_nil(@root['Some Random Name'], "Should return nil")
+ assert_nil(@root[99], "Should return nil")
+ end
+ end
+end
+
+__END__
+
+# $Log: test_tree.rb,v $
+# Revision 1.6 2007/12/22 00:28:59 anupamsg
+# Added more test cases, and enabled ZenTest compatibility.
+#
+# Revision 1.5 2007/12/19 02:24:18 anupamsg
+# Updated the marshalling logic to handle non-string contents on the nodes.
+#
+# Revision 1.4 2007/10/02 03:38:11 anupamsg
+# Removed dependency on the redundant "Person" class.
+# (TC_TreeTest::test_comparator): Added a new test for the spaceship operator.
+# (TC_TreeTest::test_hasContent): Added tests for hasContent? and length methods.
+#
+# Revision 1.3 2007/10/02 03:07:30 anupamsg
+# * Rakefile: Added an optional task for rcov code coverage.
+#
+# * test/test_binarytree.rb: Removed the unnecessary dependency on "Person" class.
+#
+# * test/test_tree.rb: Removed dependency on the redundant "Person" class.
+#
+# Revision 1.2 2007/08/31 01:16:28 anupamsg
+# Added breadth and pre-order traversals for the tree. Also added a method
+# to return the detached copy of a node from the tree.
+#
+# Revision 1.1 2007/07/21 04:52:38 anupamsg
+# Renamed the test files.
+#
+# Revision 1.13 2007/07/18 22:11:50 anupamsg
+# Added depth and breadth methods for the TreeNode.
+#
+# Revision 1.12 2007/07/18 07:17:34 anupamsg
+# Fixed a issue where TreeNode.ancestors was shadowing Module.ancestors. This method
+# has been renamed to TreeNode.parentage.
+#
+# Revision 1.11 2007/07/17 03:39:29 anupamsg
+# Moved the CVS Log keyword to end of the files.
+#
--- /dev/null
+require File.dirname(__FILE__) + '/lib/acts_as_activity_provider'
+ActiveRecord::Base.send(:include, Redmine::Acts::ActivityProvider)
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 ActivityProvider
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_activity_provider(options = {})
+ unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
+ cattr_accessor :activity_provider_options
+ send :include, Redmine::Acts::ActivityProvider::InstanceMethods
+ end
+
+ options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
+ self.activity_provider_options ||= {}
+
+ # One model can provide different event types
+ # We store these options in activity_provider_options hash
+ event_type = options.delete(:type) || self.name.underscore.pluralize
+
+ options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission)
+ options[:timestamp] ||= "#{table_name}.created_on"
+ options[:find_options] ||= {}
+ options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
+ self.activity_provider_options[event_type] = options
+ end
+ end
+
+ module InstanceMethods
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ # Returns events of type event_type visible by user that occured between from and to
+ def find_events(event_type, user, from, to, options)
+ provider_options = activity_provider_options[event_type]
+ raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
+
+ scope_options = {}
+ cond = ARCondition.new
+ if from && to
+ cond.add(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
+ end
+ if options[:author]
+ return [] if provider_options[:author_key].nil?
+ cond.add(["#{provider_options[:author_key]} = ?", options[:author].id])
+ end
+ cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission]
+ scope_options[:conditions] = cond.conditions
+ if options[:limit]
+ # id and creation time should be in same order in most cases
+ scope_options[:order] = "#{table_name}.id DESC"
+ scope_options[:limit] = options[:limit]
+ end
+
+ with_scope(:find => scope_options) do
+ find(:all, provider_options[:find_options].dup)
+ end
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/lib/acts_as_attachable'
+ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Acts
+ module Attachable
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_attachable(options = {})
+ cattr_accessor :attachable_options
+ self.attachable_options = {}
+ attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
+ attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
+
+ has_many :attachments, options.merge(:as => :container,
+ :order => "#{Attachment.table_name}.created_on",
+ :dependent => :destroy)
+ send :include, Redmine::Acts::Attachable::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ def attachments_visible?(user=User.current)
+ user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
+ end
+
+ def attachments_deletable?(user=User.current)
+ user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
+ end
+
+ module ClassMethods
+ end
+ end
+ end
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/lib/acts_as_customizable'
+ActiveRecord::Base.send(:include, Redmine::Acts::Customizable)
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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 Customizable
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_customizable(options = {})
+ return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
+ cattr_accessor :customizable_options
+ self.customizable_options = options
+ has_many :custom_values, :as => :customized,
+ :include => :custom_field,
+ :order => "#{CustomField.table_name}.position",
+ :dependent => :delete_all
+ before_validation_on_create { |customized| customized.custom_field_values }
+ # Trigger validation only if custom values were changed
+ validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
+ send :include, Redmine::Acts::Customizable::InstanceMethods
+ # Save custom values when saving the customized object
+ after_save :save_custom_field_values
+ end
+ end
+
+ module InstanceMethods
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ def available_custom_fields
+ CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
+ :order => 'position')
+ end
+
+ def custom_field_values=(values)
+ @custom_field_values_changed = true
+ values = values.stringify_keys
+ custom_field_values.each do |custom_value|
+ custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
+ end if values.is_a?(Hash)
+ end
+
+ def custom_field_values
+ @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) }
+ end
+
+ def custom_field_values_changed?
+ @custom_field_values_changed == true
+ end
+
+ def custom_value_for(c)
+ field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
+ custom_values.detect {|v| v.custom_field_id == field_id }
+ end
+
+ def save_custom_field_values
+ custom_field_values.each(&:save)
+ @custom_field_values_changed = false
+ @custom_field_values = nil
+ end
+
+ module ClassMethods
+ end
+ end
+ end
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/lib/acts_as_event'
+ActiveRecord::Base.send(:include, Redmine::Acts::Event)
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Acts
+ module Event
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_event(options = {})
+ return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
+ default_options = { :datetime => :created_on,
+ :title => :title,
+ :description => :description,
+ :author => :author,
+ :url => {:controller => 'welcome'},
+ :type => self.name.underscore.dasherize }
+
+ cattr_accessor :event_options
+ self.event_options = default_options.merge(options)
+ send :include, Redmine::Acts::Event::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ %w(datetime title description author type).each do |attr|
+ src = <<-END_SRC
+ def event_#{attr}
+ option = event_options[:#{attr}]
+ if option.is_a?(Proc)
+ option.call(self)
+ elsif option.is_a?(Symbol)
+ send(option)
+ else
+ option
+ end
+ end
+ END_SRC
+ class_eval src, __FILE__, __LINE__
+ end
+
+ def event_date
+ event_datetime.to_date
+ end
+
+ def event_url(options = {})
+ option = event_options[:url]
+ (option.is_a?(Proc) ? option.call(self) : send(option)).merge(options)
+ end
+
+ module ClassMethods
+ end
+ end
+ end
+ end
+end
--- /dev/null
+ActsAsList
+==========
+
+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.
+
+
+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
+
+
+Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
\ No newline at end of file
--- /dev/null
+$:.unshift "#{File.dirname(__FILE__)}/lib"
+require 'active_record/acts/list'
+ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
--- /dev/null
+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 <tt>_id</tt>
+ # (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: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
+ 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 <tt>acts_as_list</tt> specified. Each method works
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> 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
+ end
+
+ # Removes the item from the list.
+ def remove_from_list
+ if in_list?
+ decrement_positions_on_lower_items
+ update_attribute position_column, nil
+ end
+ end
+
+ # Increase the position of this item without adjusting the rest of the list.
+ def increment_position
+ return unless in_list?
+ update_attribute position_column, self.send(position_column).to_i + 1
+ end
+
+ # Decrease the position of this item without adjusting the rest of the list.
+ def decrement_position
+ return unless in_list?
+ update_attribute position_column, self.send(position_column).to_i - 1
+ end
+
+ # Return +true+ if this object is the first in the list.
+ def first?
+ return false unless in_list?
+ self.send(position_column) == 1
+ end
+
+ # Return +true+ if this object is the last in the list.
+ def last?
+ return false unless in_list?
+ self.send(position_column) == bottom_position_in_list
+ end
+
+ # Return the next higher item in the list.
+ def higher_item
+ return nil unless in_list?
+ acts_as_list_class.find(:first, :conditions =>
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
+ )
+ end
+
+ # Return the next lower item in the list.
+ def lower_item
+ return nil unless in_list?
+ acts_as_list_class.find(:first, :conditions =>
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
+ )
+ end
+
+ # Test if this record is in a list
+ def in_list?
+ !send(position_column).nil?
+ end
+
+ private
+ def add_to_list_top
+ increment_positions_on_all_items
+ end
+
+ def add_to_list_bottom
+ self[position_column] = bottom_position_in_list.to_i + 1
+ end
+
+ # Overwrite this method to define the scope of the list changes
+ def scope_condition() "1" end
+
+ # Returns the bottom position number in the list.
+ # bottom_position_in_list # => 2
+ def bottom_position_in_list(except = nil)
+ item = bottom_item(except)
+ item ? item.send(position_column) : 0
+ end
+
+ # Returns the bottom item
+ def bottom_item(except = nil)
+ conditions = scope_condition
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
+ 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 (<tt>position_column</tt>) 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
--- /dev/null
+require 'test/unit'
+
+require 'rubygems'
+gem 'activerecord', '>= 1.15.4.7794'
+require 'active_record'
+
+require "#{File.dirname(__FILE__)}/../init"
+
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+
+def setup_db
+ ActiveRecord::Schema.define(:version => 1) do
+ create_table :mixins do |t|
+ t.column :pos, :integer
+ t.column :parent_id, :integer
+ t.column :created_at, :datetime
+ t.column :updated_at, :datetime
+ end
+ end
+end
+
+def teardown_db
+ ActiveRecord::Base.connection.tables.each do |table|
+ ActiveRecord::Base.connection.drop_table(table)
+ end
+end
+
+class Mixin < ActiveRecord::Base
+end
+
+class ListMixin < Mixin
+ acts_as_list :column => "pos", :scope => :parent
+
+ def self.table_name() "mixins" end
+end
+
+class ListMixinSub1 < ListMixin
+end
+
+class ListMixinSub2 < ListMixin
+end
+
+class ListWithStringScopeMixin < ActiveRecord::Base
+ acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
+
+ def self.table_name() "mixins" end
+end
+
+
+class ListTest < Test::Unit::TestCase
+
+ def setup
+ setup_db
+ (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
+ end
+
+ def teardown
+ teardown_db
+ end
+
+ def test_reordering
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_lower
+ assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_higher
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(1).move_to_bottom
+ assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(1).move_to_top
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_to_bottom
+ assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(4).move_to_top
+ assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+ end
+
+ def test_move_to_bottom_with_next_to_last_item
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+ ListMixin.find(3).move_to_bottom
+ assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+ end
+
+ def test_next_prev
+ assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
+ assert_nil ListMixin.find(1).higher_item
+ assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
+ assert_nil ListMixin.find(4).lower_item
+ end
+
+ def test_injection
+ item = ListMixin.new(:parent_id => 1)
+ assert_equal "parent_id = 1", item.scope_condition
+ assert_equal "pos", item.position_column
+ end
+
+ def test_insert
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 1, new.pos
+ assert new.first?
+ assert new.last?
+
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 2, new.pos
+ assert !new.first?
+ assert new.last?
+
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 3, new.pos
+ assert !new.first?
+ assert new.last?
+
+ new = ListMixin.create(:parent_id => 0)
+ assert_equal 1, new.pos
+ assert new.first?
+ assert new.last?
+ end
+
+ def test_insert_at
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 1, new.pos
+
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 2, new.pos
+
+ new = ListMixin.create(:parent_id => 20)
+ assert_equal 3, new.pos
+
+ new4 = ListMixin.create(:parent_id => 20)
+ assert_equal 4, new4.pos
+
+ new4.insert_at(3)
+ assert_equal 3, new4.pos
+
+ new.reload
+ assert_equal 4, new.pos
+
+ new.insert_at(2)
+ assert_equal 2, new.pos
+
+ new4.reload
+ assert_equal 4, new4.pos
+
+ new5 = ListMixin.create(:parent_id => 20)
+ assert_equal 5, new5.pos
+
+ new5.insert_at(1)
+ assert_equal 1, new5.pos
+
+ new4.reload
+ assert_equal 5, new4.pos
+ end
+
+ def test_delete_middle
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).destroy
+
+ assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(1).pos
+ assert_equal 2, ListMixin.find(3).pos
+ assert_equal 3, ListMixin.find(4).pos
+
+ ListMixin.find(1).destroy
+
+ assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(3).pos
+ assert_equal 2, ListMixin.find(4).pos
+ end
+
+ def test_with_string_based_scope
+ new = ListWithStringScopeMixin.create(:parent_id => 500)
+ assert_equal 1, new.pos
+ assert new.first?
+ assert new.last?
+ end
+
+ def test_nil_scope
+ new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
+ new2.move_higher
+ assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
+ end
+
+
+ def test_remove_from_list_should_then_fail_in_list?
+ assert_equal true, ListMixin.find(1).in_list?
+ ListMixin.find(1).remove_from_list
+ assert_equal false, ListMixin.find(1).in_list?
+ end
+
+ def test_remove_from_list_should_set_position_to_nil
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).remove_from_list
+
+ assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(1).pos
+ assert_equal nil, ListMixin.find(2).pos
+ assert_equal 2, ListMixin.find(3).pos
+ assert_equal 3, ListMixin.find(4).pos
+ end
+
+ def test_remove_before_destroy_does_not_shift_lower_items_twice
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).remove_from_list
+ ListMixin.find(2).destroy
+
+ assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(1).pos
+ assert_equal 2, ListMixin.find(3).pos
+ assert_equal 3, ListMixin.find(4).pos
+ end
+
+end
+
+class ListSubTest < Test::Unit::TestCase
+
+ def setup
+ setup_db
+ (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
+ end
+
+ def teardown
+ teardown_db
+ end
+
+ def test_reordering
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_lower
+ assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_higher
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(1).move_to_bottom
+ assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(1).move_to_top
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).move_to_bottom
+ assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(4).move_to_top
+ assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+ end
+
+ def test_move_to_bottom_with_next_to_last_item
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+ ListMixin.find(3).move_to_bottom
+ assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+ end
+
+ def test_next_prev
+ assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
+ assert_nil ListMixin.find(1).higher_item
+ assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
+ assert_nil ListMixin.find(4).lower_item
+ end
+
+ def test_injection
+ item = ListMixin.new("parent_id"=>1)
+ assert_equal "parent_id = 1", item.scope_condition
+ assert_equal "pos", item.position_column
+ end
+
+ def test_insert_at
+ new = ListMixin.create("parent_id" => 20)
+ assert_equal 1, new.pos
+
+ new = ListMixinSub1.create("parent_id" => 20)
+ assert_equal 2, new.pos
+
+ new = ListMixinSub2.create("parent_id" => 20)
+ assert_equal 3, new.pos
+
+ new4 = ListMixin.create("parent_id" => 20)
+ assert_equal 4, new4.pos
+
+ new4.insert_at(3)
+ assert_equal 3, new4.pos
+
+ new.reload
+ assert_equal 4, new.pos
+
+ new.insert_at(2)
+ assert_equal 2, new.pos
+
+ new4.reload
+ assert_equal 4, new4.pos
+
+ new5 = ListMixinSub1.create("parent_id" => 20)
+ assert_equal 5, new5.pos
+
+ new5.insert_at(1)
+ assert_equal 1, new5.pos
+
+ new4.reload
+ assert_equal 5, new4.pos
+ end
+
+ def test_delete_middle
+ assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ ListMixin.find(2).destroy
+
+ assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(1).pos
+ assert_equal 2, ListMixin.find(3).pos
+ assert_equal 3, ListMixin.find(4).pos
+
+ ListMixin.find(1).destroy
+
+ assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
+
+ assert_equal 1, ListMixin.find(3).pos
+ assert_equal 2, ListMixin.find(4).pos
+ end
+
+end
--- /dev/null
+require File.dirname(__FILE__) + '/lib/acts_as_searchable'
+ActiveRecord::Base.send(:include, Redmine::Acts::Searchable)
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public 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]
+
+ # Permission needed to search this model
+ searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
+
+ # 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={})
+ tokens = [] << tokens unless tokens.is_a?(Array)
+ projects = [] << projects unless projects.nil? || projects.is_a?(Array)
+
+ find_options = {:include => searchable_options[:include]}
+ find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
+
+ limit_options = {}
+ limit_options[:limit] = options[:limit] if options[:limit]
+ if options[:offset]
+ limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
+ end
+
+ 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_field_ids = CustomField.find(:all,
+ :select => 'id',
+ :conditions => { :type => "#{self.name}CustomField",
+ :searchable => true }).collect(&:id)
+ if searchable_custom_field_ids.any?
+ custom_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 IN (#{searchable_custom_field_ids.join(',')}))"
+ token_clauses << custom_field_sql
+ end
+ end
+
+ sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
+
+ find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
+
+ project_conditions = []
+ project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
+ Project.allowed_to_condition(User.current, searchable_options[:permission]))
+ project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
+
+ results = []
+ results_count = 0
+
+ with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
+ with_scope(:find => find_options) do
+ results_count = count(:all)
+ results = find(:all, limit_options)
+ end
+ end
+ [results, results_count]
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+acts_as_tree
+============
+
+Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
+association. This requires that you have a foreign key column, which by default is called +parent_id+.
+
+ class Category < ActiveRecord::Base
+ acts_as_tree :order => "name"
+ end
+
+ Example:
+ root
+ \_ child1
+ \_ subchild1
+ \_ subchild2
+
+ root = Category.create("name" => "root")
+ child1 = root.children.create("name" => "child1")
+ subchild1 = child1.children.create("name" => "subchild1")
+
+ root.parent # => nil
+ child1.parent # => root
+ root.children # => [child1]
+ root.children.first.children.first # => subchild1
+
+Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
\ No newline at end of file
--- /dev/null
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test acts_as_tree plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for acts_as_tree plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'acts_as_tree'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
--- /dev/null
+ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
--- /dev/null
+module ActiveRecord
+ module Acts
+ module Tree
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+.
+ #
+ # class Category < ActiveRecord::Base
+ # acts_as_tree :order => "name"
+ # end
+ #
+ # Example:
+ # root
+ # \_ child1
+ # \_ subchild1
+ # \_ subchild2
+ #
+ # root = Category.create("name" => "root")
+ # child1 = root.children.create("name" => "child1")
+ # subchild1 = child1.children.create("name" => "subchild1")
+ #
+ # root.parent # => nil
+ # child1.parent # => root
+ # root.children # => [child1]
+ # root.children.first.children.first # => subchild1
+ #
+ # In addition to the parent and children associations, the following instance methods are added to the class
+ # after calling <tt>acts_as_tree</tt>:
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
+ # * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
+ module ClassMethods
+ # Configuration options are:
+ #
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
+ def acts_as_tree(options = {})
+ configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil }
+ configuration.update(options) if options.is_a?(Hash)
+
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent]
+
+ class_eval <<-EOV
+ include ActiveRecord::Acts::Tree::InstanceMethods
+
+ def self.roots
+ find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
+ end
+
+ def self.root
+ find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
+ end
+ EOV
+ end
+ end
+
+ module InstanceMethods
+ # Returns list of ancestors, starting from parent until root.
+ #
+ # subchild1.ancestors # => [child1, root]
+ def ancestors
+ node, nodes = self, []
+ nodes << node = node.parent while node.parent
+ nodes
+ end
+
+ # Returns list of descendants.
+ #
+ # root.descendants # => [child1, subchild1, subchild2]
+ def descendants
+ children + children.collect(&:children).flatten
+ end
+
+ # Returns list of descendants and a reference to the current node.
+ #
+ # root.self_and_descendants # => [root, child1, subchild1, subchild2]
+ def self_and_descendants
+ [self] + descendants
+ end
+
+ # Returns the root node of the tree.
+ def root
+ node = self
+ node = node.parent while node.parent
+ node
+ end
+
+ # Returns all siblings of the current node.
+ #
+ # subchild1.siblings # => [subchild2]
+ def siblings
+ self_and_siblings - [self]
+ end
+
+ # Returns all siblings and a reference to the current node.
+ #
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
+ def self_and_siblings
+ parent ? parent.children : self.class.roots
+ end
+ end
+ end
+ end
+end
--- /dev/null
+require 'test/unit'
+
+require 'rubygems'
+require 'active_record'
+
+$:.unshift File.dirname(__FILE__) + '/../lib'
+require File.dirname(__FILE__) + '/../init'
+
+class Test::Unit::TestCase
+ def assert_queries(num = 1)
+ $query_count = 0
+ yield
+ ensure
+ assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
+ end
+
+ def assert_no_queries(&block)
+ assert_queries(0, &block)
+ end
+end
+
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+
+# AR keeps printing annoying schema statements
+$stdout = StringIO.new
+
+def setup_db
+ ActiveRecord::Base.logger
+ ActiveRecord::Schema.define(:version => 1) do
+ create_table :mixins do |t|
+ t.column :type, :string
+ t.column :parent_id, :integer
+ end
+ end
+end
+
+def teardown_db
+ ActiveRecord::Base.connection.tables.each do |table|
+ ActiveRecord::Base.connection.drop_table(table)
+ end
+end
+
+class Mixin < ActiveRecord::Base
+end
+
+class TreeMixin < Mixin
+ acts_as_tree :foreign_key => "parent_id", :order => "id"
+end
+
+class TreeMixinWithoutOrder < Mixin
+ acts_as_tree :foreign_key => "parent_id"
+end
+
+class RecursivelyCascadedTreeMixin < Mixin
+ acts_as_tree :foreign_key => "parent_id"
+ has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
+end
+
+class TreeTest < Test::Unit::TestCase
+
+ def setup
+ setup_db
+ @root1 = TreeMixin.create!
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
+ @root2 = TreeMixin.create!
+ @root3 = TreeMixin.create!
+ end
+
+ def teardown
+ teardown_db
+ end
+
+ def test_children
+ assert_equal @root1.children, [@root_child1, @root_child2]
+ assert_equal @root_child1.children, [@child1_child]
+ assert_equal @child1_child.children, []
+ assert_equal @root_child2.children, []
+ end
+
+ def test_parent
+ assert_equal @root_child1.parent, @root1
+ assert_equal @root_child1.parent, @root_child2.parent
+ assert_nil @root1.parent
+ end
+
+ def test_delete
+ assert_equal 6, TreeMixin.count
+ @root1.destroy
+ assert_equal 2, TreeMixin.count
+ @root2.destroy
+ @root3.destroy
+ assert_equal 0, TreeMixin.count
+ end
+
+ def test_insert
+ @extra = @root1.children.create
+
+ assert @extra
+
+ assert_equal @extra.parent, @root1
+
+ assert_equal 3, @root1.children.size
+ assert @root1.children.include?(@extra)
+ assert @root1.children.include?(@root_child1)
+ assert @root1.children.include?(@root_child2)
+ end
+
+ def test_ancestors
+ assert_equal [], @root1.ancestors
+ assert_equal [@root1], @root_child1.ancestors
+ assert_equal [@root_child1, @root1], @child1_child.ancestors
+ assert_equal [@root1], @root_child2.ancestors
+ assert_equal [], @root2.ancestors
+ assert_equal [], @root3.ancestors
+ end
+
+ def test_root
+ assert_equal @root1, TreeMixin.root
+ assert_equal @root1, @root1.root
+ assert_equal @root1, @root_child1.root
+ assert_equal @root1, @child1_child.root
+ assert_equal @root1, @root_child2.root
+ assert_equal @root2, @root2.root
+ assert_equal @root3, @root3.root
+ end
+
+ def test_roots
+ assert_equal [@root1, @root2, @root3], TreeMixin.roots
+ end
+
+ def test_siblings
+ assert_equal [@root2, @root3], @root1.siblings
+ assert_equal [@root_child2], @root_child1.siblings
+ assert_equal [], @child1_child.siblings
+ assert_equal [@root_child1], @root_child2.siblings
+ assert_equal [@root1, @root3], @root2.siblings
+ assert_equal [@root1, @root2], @root3.siblings
+ end
+
+ def test_self_and_siblings
+ assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
+ assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
+ assert_equal [@child1_child], @child1_child.self_and_siblings
+ assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
+ assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
+ assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
+ end
+end
+
+class TreeTestWithEagerLoading < Test::Unit::TestCase
+
+ def setup
+ teardown_db
+ setup_db
+ @root1 = TreeMixin.create!
+ @root_child1 = TreeMixin.create! :parent_id => @root1.id
+ @child1_child = TreeMixin.create! :parent_id => @root_child1.id
+ @root_child2 = TreeMixin.create! :parent_id => @root1.id
+ @root2 = TreeMixin.create!
+ @root3 = TreeMixin.create!
+
+ @rc1 = RecursivelyCascadedTreeMixin.create!
+ @rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
+ @rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
+ @rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
+ end
+
+ def teardown
+ teardown_db
+ end
+
+ def test_eager_association_loading
+ roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
+ assert_equal [@root1, @root2, @root3], roots
+ assert_no_queries do
+ assert_equal 2, roots[0].children.size
+ assert_equal 0, roots[1].children.size
+ assert_equal 0, roots[2].children.size
+ end
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id')
+ assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
+ root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id')
+ assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
+ end
+
+ def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
+ leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
+ assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
+ end
+end
+
+class TreeTestWithoutOrder < Test::Unit::TestCase
+
+ def setup
+ setup_db
+ @root1 = TreeMixinWithoutOrder.create!
+ @root2 = TreeMixinWithoutOrder.create!
+ end
+
+ def teardown
+ teardown_db
+ end
+
+ def test_root
+ assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
+ end
+
+ def test_roots
+ assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
+ end
+end
--- /dev/null
+*SVN* (version numbers are overrated)
+
+* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
+
+*0.5.1*
+
+* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
+
+*0.5* # do versions even matter for plugins?
+
+* (21 Apr 2006) Added without_locking and without_revision methods.
+
+ Foo.without_revision do
+ @foo.update_attributes ...
+ end
+
+*0.4*
+
+* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
+* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
+
+*0.3.1*
+
+* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
+* (7 Jan 2006) added tests to prove has_many :through joins work
+
+*0.3*
+
+* (2 Jan 2006) added ability to share a mixin with versioned class
+* (2 Jan 2006) changed the dynamic version model to MyModel::Version
+
+*0.2.4*
+
+* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
+
+*0.2.3*
+
+* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
+* (12 Nov 2005) updated tests to use ActiveRecord Schema
+
+*0.2.2*
+
+* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
+
+*0.2.1*
+
+* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
+
+*0.2*
+
+* (6 Oct 2005) added find_versions and find_version class methods.
+
+* (6 Oct 2005) removed transaction from create_versioned_table().
+ this way you can specify your own transaction around a group of operations.
+
+* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
+
+* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
+
+*0.1.3* (18 Sep 2005)
+
+* First RubyForge release
+
+*0.1.2*
+
+* check if module is already included when acts_as_versioned is called
+
+*0.1.1*
+
+* Adding tests and rdocs
+
+*0.1*
+
+* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974
\ No newline at end of file
--- /dev/null
+Copyright (c) 2005 Rick Olson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
--- /dev/null
+= acts_as_versioned
+
+This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
+
+== Resources
+
+Install
+
+* gem install acts_as_versioned
+
+Rubyforge project
+
+* http://rubyforge.org/projects/ar-versioned
+
+RDocs
+
+* http://ar-versioned.rubyforge.org
+
+Subversion
+
+* http://techno-weenie.net/svn/projects/acts_as_versioned
+
+Collaboa
+
+* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
+
+Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
+was the first project to use acts_as_versioned <em>in the wild</em>.
\ No newline at end of file
--- /dev/null
+== Creating the test database
+
+The default name for the test databases is "activerecord_versioned". If you
+want to use another database name then be sure to update the connection
+adapter setups you want to test with in test/connections/<your database>/connection.rb.
+When you have the database online, you can import the fixture tables with
+the test/fixtures/db_definitions/*.sql files.
+
+Make sure that you create database objects with the same user that you specified in i
+connection.rb otherwise (on Postgres, at least) tests for default values will fail.
+
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all the adapters. You can also run the suite on just
+one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
+or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+Unit tests are located in test directory. If you only want to run a single test suite,
+or don't want to bother with Rake, you can do so with something like:
+
+ cd test; ruby -I "connections/native_mysql" base_test.rb
+
+That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
+and test suite name as needed.
+
+== Faster tests
+
+If you are using a database that supports transactions, you can set the
+"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
+This gives a very large speed boost. With rake:
+
+ rake AR_TX_FIXTURES=yes
+
+Or, by hand:
+
+ AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
--- /dev/null
+require 'rubygems'\r
+\r
+Gem::manage_gems\r
+\r
+require 'rake/rdoctask'\r
+require 'rake/packagetask'\r
+require 'rake/gempackagetask'\r
+require 'rake/testtask'\r
+require 'rake/contrib/rubyforgepublisher'\r
+\r
+PKG_NAME = 'acts_as_versioned'\r
+PKG_VERSION = '0.3.1'\r
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"\r
+PROD_HOST = "technoweenie@bidwell.textdrive.com"\r
+RUBY_FORGE_PROJECT = 'ar-versioned'\r
+RUBY_FORGE_USER = 'technoweenie'\r
+\r
+desc 'Default: run unit tests.'\r
+task :default => :test\r
+\r
+desc 'Test the calculations plugin.'\r
+Rake::TestTask.new(:test) do |t|\r
+ t.libs << 'lib'\r
+ t.pattern = 'test/**/*_test.rb'\r
+ t.verbose = true\r
+end\r
+\r
+desc 'Generate documentation for the calculations plugin.'\r
+Rake::RDocTask.new(:rdoc) do |rdoc|\r
+ rdoc.rdoc_dir = 'rdoc'\r
+ rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"\r
+ rdoc.options << '--line-numbers --inline-source'\r
+ rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')\r
+ rdoc.rdoc_files.include('lib/**/*.rb')\r
+end\r
+\r
+spec = Gem::Specification.new do |s|\r
+ s.name = PKG_NAME\r
+ s.version = PKG_VERSION\r
+ s.platform = Gem::Platform::RUBY\r
+ s.summary = "Simple versioning with active record models"\r
+ s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)\r
+ s.files.delete "acts_as_versioned_plugin.sqlite.db"\r
+ s.files.delete "acts_as_versioned_plugin.sqlite3.db"\r
+ s.files.delete "test/debug.log"\r
+ s.require_path = 'lib'\r
+ s.autorequire = 'acts_as_versioned'\r
+ s.has_rdoc = true\r
+ s.test_files = Dir['test/**/*_test.rb']\r
+ s.add_dependency 'activerecord', '>= 1.10.1'\r
+ s.add_dependency 'activesupport', '>= 1.1.1'\r
+ s.author = "Rick Olson"\r
+ s.email = "technoweenie@gmail.com"\r
+ s.homepage = "http://techno-weenie.net"\r
+end\r
+\r
+Rake::GemPackageTask.new(spec) do |pkg|\r
+ pkg.need_tar = true\r
+end\r
+\r
+desc "Publish the API documentation"\r
+task :pdoc => [:rdoc] do\r
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload\r
+end\r
+\r
+desc 'Publish the gem and API docs'\r
+task :publish => [:pdoc, :rubyforge_upload]\r
+\r
+desc "Publish the release files to RubyForge."\r
+task :rubyforge_upload => :package do\r
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }\r
+\r
+ if RUBY_FORGE_PROJECT then\r
+ require 'net/http'\r
+ require 'open-uri'\r
+\r
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"\r
+ project_data = open(project_uri) { |data| data.read }\r
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]\r
+ raise "Couldn't get group id" unless group_id\r
+\r
+ # This echos password to shell which is a bit sucky\r
+ if ENV["RUBY_FORGE_PASSWORD"]\r
+ password = ENV["RUBY_FORGE_PASSWORD"]\r
+ else\r
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "\r
+ password = STDIN.gets.chomp\r
+ end\r
+\r
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|\r
+ data = [\r
+ "login=1",\r
+ "form_loginname=#{RUBY_FORGE_USER}",\r
+ "form_pw=#{password}"\r
+ ].join("&")\r
+ http.post("/account/login.php", data)\r
+ end\r
+\r
+ cookie = login_response["set-cookie"]\r
+ raise "Login failed" unless cookie\r
+ headers = { "Cookie" => cookie }\r
+\r
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"\r
+ release_data = open(release_uri, headers) { |data| data.read }\r
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]\r
+ raise "Couldn't get package id" unless package_id\r
+\r
+ first_file = true\r
+ release_id = ""\r
+\r
+ files.each do |filename|\r
+ basename = File.basename(filename)\r
+ file_ext = File.extname(filename)\r
+ file_data = File.open(filename, "rb") { |file| file.read }\r
+\r
+ puts "Releasing #{basename}..."\r
+\r
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|\r
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")\r
+ type_map = {\r
+ ".zip" => "3000",\r
+ ".tgz" => "3110",\r
+ ".gz" => "3110",\r
+ ".gem" => "1400"\r
+ }; type_map.default = "9999"\r
+ type = type_map[file_ext]\r
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"\r
+\r
+ query_hash = if first_file then\r
+ {\r
+ "group_id" => group_id,\r
+ "package_id" => package_id,\r
+ "release_name" => PKG_FILE_NAME,\r
+ "release_date" => release_date,\r
+ "type_id" => type,\r
+ "processor_id" => "8000", # Any\r
+ "release_notes" => "",\r
+ "release_changes" => "",\r
+ "preformatted" => "1",\r
+ "submit" => "1"\r
+ }\r
+ else\r
+ {\r
+ "group_id" => group_id,\r
+ "release_id" => release_id,\r
+ "package_id" => package_id,\r
+ "step2" => "1",\r
+ "type_id" => type,\r
+ "processor_id" => "8000", # Any\r
+ "submit" => "Add This File"\r
+ }\r
+ end\r
+\r
+ query = "?" + query_hash.map do |(name, value)|\r
+ [name, URI.encode(value)].join("=")\r
+ end.join("&")\r
+\r
+ data = [\r
+ "--" + boundary,\r
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",\r
+ "Content-Type: application/octet-stream",\r
+ "Content-Transfer-Encoding: binary",\r
+ "", file_data, ""\r
+ ].join("\x0D\x0A")\r
+\r
+ release_headers = headers.merge(\r
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"\r
+ )\r
+\r
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"\r
+ http.post(target + query, data, release_headers)\r
+ end\r
+\r
+ if first_file then\r
+ release_id = release_response.body[/release_id=(\d+)/, 1]\r
+ raise("Couldn't get release id") unless release_id\r
+ end\r
+\r
+ first_file = false\r
+ end\r
+ end\r
+end
\ No newline at end of file
--- /dev/null
+require 'acts_as_versioned'
\ No newline at end of file
--- /dev/null
+# Copyright (c) 2005 Rick Olson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+module ActiveRecord #:nodoc:
+ module Acts #:nodoc:
+ # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
+ # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
+ # column is present as well.
+ #
+ # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
+ # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
+ #
+ # class Page < ActiveRecord::Base
+ # # assumes pages_versions table
+ # acts_as_versioned
+ # end
+ #
+ # Example:
+ #
+ # page = Page.create(:title => 'hello world!')
+ # page.version # => 1
+ #
+ # page.title = 'hello world'
+ # page.save
+ # page.version # => 2
+ # page.versions.size # => 2
+ #
+ # page.revert_to(1) # using version number
+ # page.title # => 'hello world!'
+ #
+ # page.revert_to(page.versions.last) # using versioned instance
+ # page.title # => 'hello world'
+ #
+ # page.versions.earliest # efficient query to find the first version
+ # page.versions.latest # efficient query to find the most recently created version
+ #
+ #
+ # Simple Queries to page between versions
+ #
+ # page.versions.before(version)
+ # page.versions.after(version)
+ #
+ # Access the previous/next versions from the versioned model itself
+ #
+ # version = page.versions.latest
+ # version.previous # go back one version
+ # version.next # go forward one version
+ #
+ # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
+ module Versioned
+ CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
+ def self.included(base) # :nodoc:
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ # == Configuration options
+ #
+ # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
+ # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
+ # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
+ # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
+ # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
+ # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
+ # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
+ # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
+ # For finer control, pass either a Proc or modify Model#version_condition_met?
+ #
+ # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
+ #
+ # or...
+ #
+ # class Auction
+ # def version_condition_met? # totally bypasses the <tt>:if</tt> option
+ # !expired?
+ # end
+ # end
+ #
+ # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
+ # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
+ # Use this instead if you want to write your own attribute setters (and ignore if_changed):
+ #
+ # def name=(new_name)
+ # write_changed_attribute :name, new_name
+ # end
+ #
+ # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
+ # to create an anonymous mixin:
+ #
+ # class Auction
+ # acts_as_versioned do
+ # def started?
+ # !started_at.nil?
+ # end
+ # end
+ # end
+ #
+ # or...
+ #
+ # module AuctionExtension
+ # def started?
+ # !started_at.nil?
+ # end
+ # end
+ # class Auction
+ # acts_as_versioned :extend => AuctionExtension
+ # end
+ #
+ # Example code:
+ #
+ # @auction = Auction.find(1)
+ # @auction.started?
+ # @auction.versions.first.started?
+ #
+ # == Database Schema
+ #
+ # The model that you're versioning needs to have a 'version' attribute. The model is versioned
+ # into a table called #{model}_versions where the model name is singlular. The _versions table should
+ # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
+ #
+ # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
+ # then that field is reflected in the versioned model as 'versioned_type' by default.
+ #
+ # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
+ # method, perfect for a migration. It will also create the version column if the main model does not already have it.
+ #
+ # class AddVersions < ActiveRecord::Migration
+ # def self.up
+ # # create_versioned_table takes the same options hash
+ # # that create_table does
+ # Post.create_versioned_table
+ # end
+ #
+ # def self.down
+ # Post.drop_versioned_table
+ # end
+ # end
+ #
+ # == Changing What Fields Are Versioned
+ #
+ # By default, acts_as_versioned will version all but these fields:
+ #
+ # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
+ #
+ # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
+ #
+ # class Post < ActiveRecord::Base
+ # acts_as_versioned
+ # self.non_versioned_columns << 'comments_count'
+ # end
+ #
+ def acts_as_versioned(options = {}, &extension)
+ # don't allow multiple calls
+ return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
+
+ send :include, ActiveRecord::Acts::Versioned::ActMethods
+
+ cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
+ :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
+ :version_association_options
+
+ # legacy
+ alias_method :non_versioned_fields, :non_versioned_columns
+ alias_method :non_versioned_fields=, :non_versioned_columns=
+
+ class << self
+ alias_method :non_versioned_fields, :non_versioned_columns
+ alias_method :non_versioned_fields=, :non_versioned_columns=
+ end
+
+ send :attr_accessor, :altered_attributes
+
+ self.versioned_class_name = options[:class_name] || "Version"
+ self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
+ self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
+ self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
+ self.version_column = options[:version_column] || 'version'
+ self.version_sequence_name = options[:sequence_name]
+ self.max_version_limit = options[:limit].to_i
+ self.version_condition = options[:if] || true
+ self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
+ self.version_association_options = {
+ :class_name => "#{self.to_s}::#{versioned_class_name}",
+ :foreign_key => versioned_foreign_key,
+ :dependent => :delete_all
+ }.merge(options[:association_options] || {})
+
+ if block_given?
+ extension_module_name = "#{versioned_class_name}Extension"
+ silence_warnings do
+ self.const_set(extension_module_name, Module.new(&extension))
+ end
+
+ options[:extend] = self.const_get(extension_module_name)
+ end
+
+ class_eval do
+ has_many :versions, version_association_options do
+ # finds earliest version of this record
+ def earliest
+ @earliest ||= find(:first, :order => 'version')
+ end
+
+ # find latest version of this record
+ def latest
+ @latest ||= find(:first, :order => 'version desc')
+ end
+ end
+ before_save :set_new_version
+ after_create :save_version_on_create
+ after_update :save_version
+ after_save :clear_old_versions
+ after_save :clear_altered_attributes
+
+ unless options[:if_changed].nil?
+ self.track_altered_attributes = true
+ options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
+ options[:if_changed].each do |attr_name|
+ define_method("#{attr_name}=") do |value|
+ write_changed_attribute attr_name, value
+ end
+ end
+ end
+
+ include options[:extend] if options[:extend].is_a?(Module)
+ end
+
+ # create the dynamic versioned model
+ const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
+ def self.reloadable? ; false ; end
+ # find first version before the given version
+ def self.before(version)
+ find :first, :order => 'version desc',
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
+ end
+
+ # find first version after the given version.
+ def self.after(version)
+ find :first, :order => 'version',
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
+ end
+
+ def previous
+ self.class.before(self)
+ end
+
+ def next
+ self.class.after(self)
+ end
+
+ def versions_count
+ page.version
+ end
+ end
+
+ versioned_class.cattr_accessor :original_class
+ versioned_class.original_class = self
+ versioned_class.set_table_name versioned_table_name
+ versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
+ :class_name => "::#{self.to_s}",
+ :foreign_key => versioned_foreign_key
+ versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
+ versioned_class.set_sequence_name version_sequence_name if version_sequence_name
+ end
+ end
+
+ module ActMethods
+ def self.included(base) # :nodoc:
+ base.extend ClassMethods
+ end
+
+ # Finds a specific version of this record
+ def find_version(version = nil)
+ self.class.find_version(id, version)
+ end
+
+ # Saves a version of the model if applicable
+ def save_version
+ save_version_on_create if save_version?
+ end
+
+ # Saves a version of the model in the versioned table. This is called in the after_save callback by default
+ def save_version_on_create
+ rev = self.class.versioned_class.new
+ self.clone_versioned_model(self, rev)
+ rev.version = send(self.class.version_column)
+ rev.send("#{self.class.versioned_foreign_key}=", self.id)
+ rev.save
+ end
+
+ # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
+ # Override this method to set your own criteria for clearing old versions.
+ def clear_old_versions
+ return if self.class.max_version_limit == 0
+ excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
+ if excess_baggage > 0
+ sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
+ self.class.versioned_class.connection.execute sql
+ end
+ end
+
+ def versions_count
+ version
+ end
+
+ # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
+ def revert_to(version)
+ if version.is_a?(self.class.versioned_class)
+ return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
+ else
+ return false unless version = versions.find_by_version(version)
+ end
+ self.clone_versioned_model(version, self)
+ self.send("#{self.class.version_column}=", version.version)
+ true
+ end
+
+ # Reverts a model to a given version and saves the model.
+ # Takes either a version number or an instance of the versioned model
+ def revert_to!(version)
+ revert_to(version) ? save_without_revision : false
+ end
+
+ # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
+ def save_without_revision
+ save_without_revision!
+ true
+ rescue
+ false
+ end
+
+ def save_without_revision!
+ without_locking do
+ without_revision do
+ save!
+ end
+ end
+ end
+
+ # Returns an array of attribute keys that are versioned. See non_versioned_columns
+ def versioned_attributes
+ self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
+ end
+
+ # If called with no parameters, gets whether the current model has changed and needs to be versioned.
+ # If called with a single parameter, gets whether the parameter has changed.
+ def changed?(attr_name = nil)
+ attr_name.nil? ?
+ (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
+ (altered_attributes && altered_attributes.include?(attr_name.to_s))
+ end
+
+ # keep old dirty? method
+ alias_method :dirty?, :changed?
+
+ # Clones a model. Used when saving a new version or reverting a model's version.
+ def clone_versioned_model(orig_model, new_model)
+ self.versioned_attributes.each do |key|
+ new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
+ end
+
+ if orig_model.is_a?(self.class.versioned_class)
+ new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
+ elsif new_model.is_a?(self.class.versioned_class)
+ new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
+ end
+ end
+
+ # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
+ def save_version?
+ version_condition_met? && changed?
+ end
+
+ # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
+ # custom version condition checking.
+ def version_condition_met?
+ case
+ when version_condition.is_a?(Symbol)
+ send(version_condition)
+ when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
+ version_condition.call(self)
+ else
+ version_condition
+ end
+ end
+
+ # Executes the block with the versioning callbacks disabled.
+ #
+ # @foo.without_revision do
+ # @foo.save
+ # end
+ #
+ def without_revision(&block)
+ self.class.without_revision(&block)
+ end
+
+ # Turns off optimistic locking for the duration of the block
+ #
+ # @foo.without_locking do
+ # @foo.save
+ # end
+ #
+ def without_locking(&block)
+ self.class.without_locking(&block)
+ end
+
+ def empty_callback() end #:nodoc:
+
+ protected
+ # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
+ def set_new_version
+ self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
+ end
+
+ # Gets the next available version for the current record, or 1 for a new record
+ def next_version
+ return 1 if new_record?
+ (versions.calculate(:max, :version) || 0) + 1
+ end
+
+ # clears current changed attributes. Called after save.
+ def clear_altered_attributes
+ self.altered_attributes = []
+ end
+
+ def write_changed_attribute(attr_name, attr_value)
+ # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
+ attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
+ (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
+ write_attribute(attr_name, attr_value_for_db)
+ end
+
+ module ClassMethods
+ # Finds a specific version of a specific row of this model
+ def find_version(id, version = nil)
+ return find(id) unless version
+
+ conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
+ options = { :conditions => conditions, :limit => 1 }
+
+ if result = find_versions(id, options).first
+ result
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
+ end
+ end
+
+ # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
+ def find_versions(id, options = {})
+ versioned_class.find :all, {
+ :conditions => ["#{versioned_foreign_key} = ?", id],
+ :order => 'version' }.merge(options)
+ end
+
+ # Returns an array of columns that are versioned. See non_versioned_columns
+ def versioned_columns
+ self.columns.select { |c| !non_versioned_columns.include?(c.name) }
+ end
+
+ # Returns an instance of the dynamic versioned model
+ def versioned_class
+ const_get versioned_class_name
+ end
+
+ # Rake migration task to create the versioned table using options passed to acts_as_versioned
+ def create_versioned_table(create_table_options = {})
+ # create version column in main table if it does not exist
+ if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
+ self.connection.add_column table_name, :version, :integer
+ end
+
+ self.connection.create_table(versioned_table_name, create_table_options) do |t|
+ t.column versioned_foreign_key, :integer
+ t.column :version, :integer
+ end
+
+ updated_col = nil
+ self.versioned_columns.each do |col|
+ updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
+ self.connection.add_column versioned_table_name, col.name, col.type,
+ :limit => col.limit,
+ :default => col.default,
+ :scale => col.scale,
+ :precision => col.precision
+ end
+
+ if type_col = self.columns_hash[inheritance_column]
+ self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
+ :limit => type_col.limit,
+ :default => type_col.default,
+ :scale => type_col.scale,
+ :precision => type_col.precision
+ end
+
+ if updated_col.nil?
+ self.connection.add_column versioned_table_name, :updated_at, :timestamp
+ end
+ end
+
+ # Rake migration task to drop the versioned table
+ def drop_versioned_table
+ self.connection.drop_table versioned_table_name
+ end
+
+ # Executes the block with the versioning callbacks disabled.
+ #
+ # Foo.without_revision do
+ # @foo.save
+ # end
+ #
+ def without_revision(&block)
+ class_eval do
+ CALLBACKS.each do |attr_name|
+ alias_method "orig_#{attr_name}".to_sym, attr_name
+ alias_method attr_name, :empty_callback
+ end
+ end
+ block.call
+ ensure
+ class_eval do
+ CALLBACKS.each do |attr_name|
+ alias_method attr_name, "orig_#{attr_name}".to_sym
+ end
+ end
+ end
+
+ # Turns off optimistic locking for the duration of the block
+ #
+ # Foo.without_locking do
+ # @foo.save
+ # end
+ #
+ def without_locking(&block)
+ current = ActiveRecord::Base.lock_optimistically
+ ActiveRecord::Base.lock_optimistically = false if current
+ result = block.call
+ ActiveRecord::Base.lock_optimistically = true if current
+ result
+ end
+ end
+ end
+ end
+ end
+end
+
+ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
\ No newline at end of file
--- /dev/null
+$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
+$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+require 'test/unit'
+begin
+ require 'active_support'
+ require 'active_record'
+ require 'active_record/fixtures'
+rescue LoadError
+ require 'rubygems'
+ retry
+end
+require 'acts_as_versioned'
+
+config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
+ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
+ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+
+load(File.dirname(__FILE__) + "/schema.rb")
+
+# set up custom sequence on widget_versions for DBs that support sequences
+if ENV['DB'] == 'postgresql'
+ ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
+ ActiveRecord::Base.connection.remove_column :widget_versions, :id
+ ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
+ ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
+end
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+$:.unshift(Test::Unit::TestCase.fixture_path)
+
+class Test::Unit::TestCase #:nodoc:
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
+ self.use_transactional_fixtures = true
+
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
+ self.use_instantiated_fixtures = false
+
+ # Add more helper methods to be used by all tests here...
+end
\ No newline at end of file
--- /dev/null
+sqlite:
+ :adapter: sqlite
+ :dbfile: acts_as_versioned_plugin.sqlite.db
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: acts_as_versioned_plugin.sqlite3.db
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: acts_as_versioned_plugin_test
+ :min_messages: ERROR
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: acts_as_versioned_plugin_test
\ No newline at end of file
--- /dev/null
+caged:
+ id: 1
+ name: caged
+mly:
+ id: 2
+ name: mly
\ No newline at end of file
--- /dev/null
+class Landmark < ActiveRecord::Base
+ acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
+end
--- /dev/null
+washington:
+ id: 1
+ landmark_id: 1
+ version: 1
+ name: Washington, D.C.
+ latitude: 38.895
+ longitude: -77.036667
--- /dev/null
+washington:
+ id: 1
+ name: Washington, D.C.
+ latitude: 38.895
+ longitude: -77.036667
+ version: 1
--- /dev/null
+welcome:
+ id: 1
+ title: Welcome to the weblog
+ lock_version: 24
+ type: LockedPage
+thinking:
+ id: 2
+ title: So I was thinking
+ lock_version: 24
+ type: SpecialLockedPage
--- /dev/null
+welcome_1:
+ id: 1
+ page_id: 1
+ title: Welcome to the weblg
+ version: 23
+ version_type: LockedPage
+
+welcome_2:
+ id: 2
+ page_id: 1
+ title: Welcome to the weblog
+ version: 24
+ version_type: LockedPage
+
+thinking_1:
+ id: 3
+ page_id: 2
+ title: So I was thinking!!!
+ version: 23
+ version_type: SpecialLockedPage
+
+thinking_2:
+ id: 4
+ page_id: 2
+ title: So I was thinking
+ version: 24
+ version_type: SpecialLockedPage
--- /dev/null
+class AddVersionedTables < ActiveRecord::Migration
+ def self.up
+ create_table("things") do |t|
+ t.column :title, :text
+ end
+ Thing.create_versioned_table
+ end
+
+ def self.down
+ Thing.drop_versioned_table
+ drop_table "things" rescue nil
+ end
+end
\ No newline at end of file
--- /dev/null
+class Page < ActiveRecord::Base
+ belongs_to :author
+ has_many :authors, :through => :versions, :order => 'name'
+ belongs_to :revisor, :class_name => 'Author'
+ has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
+ acts_as_versioned :if => :feeling_good? do
+ def self.included(base)
+ base.cattr_accessor :feeling_good
+ base.feeling_good = true
+ base.belongs_to :author
+ base.belongs_to :revisor, :class_name => 'Author'
+ end
+
+ def feeling_good?
+ @@feeling_good == true
+ end
+ end
+end
+
+module LockedPageExtension
+ def hello_world
+ 'hello_world'
+ end
+end
+
+class LockedPage < ActiveRecord::Base
+ acts_as_versioned \
+ :inheritance_column => :version_type,
+ :foreign_key => :page_id,
+ :table_name => :locked_pages_revisions,
+ :class_name => 'LockedPageRevision',
+ :version_column => :lock_version,
+ :limit => 2,
+ :if_changed => :title,
+ :extend => LockedPageExtension
+end
+
+class SpecialLockedPage < LockedPage
+end
+
+class Author < ActiveRecord::Base
+ has_many :pages
+end
\ No newline at end of file
--- /dev/null
+welcome_2:
+ id: 1
+ page_id: 1
+ title: Welcome to the weblog
+ body: Such a lovely day
+ version: 24
+ author_id: 1
+ revisor_id: 1
+welcome_1:
+ id: 2
+ page_id: 1
+ title: Welcome to the weblg
+ body: Such a lovely day
+ version: 23
+ author_id: 2
+ revisor_id: 2
--- /dev/null
+welcome:
+ id: 1
+ title: Welcome to the weblog
+ body: Such a lovely day
+ version: 24
+ author_id: 1
+ revisor_id: 1
\ No newline at end of file
--- /dev/null
+class Widget < ActiveRecord::Base
+ acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
+ :dependent => :nullify, :order => 'version desc'
+ }
+ non_versioned_columns << 'foo'
+end
\ No newline at end of file
--- /dev/null
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+
+if ActiveRecord::Base.connection.supports_migrations?
+ class Thing < ActiveRecord::Base
+ attr_accessor :version
+ acts_as_versioned
+ end
+
+ class MigrationTest < Test::Unit::TestCase
+ self.use_transactional_fixtures = false
+ def teardown
+ if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
+ ActiveRecord::Base.connection.initialize_schema_information
+ ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
+ else
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
+ ActiveRecord::Base.connection.assume_migrated_upto_version(0)
+ end
+
+ Thing.connection.drop_table "things" rescue nil
+ Thing.connection.drop_table "thing_versions" rescue nil
+ Thing.reset_column_information
+ end
+
+ def test_versioned_migration
+ assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
+ # take 'er up
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
+ t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
+ assert_equal 1, t.versions.size
+
+ # check that the price column has remembered its value correctly
+ assert_equal t.price, t.versions.first.price
+ assert_equal t.title, t.versions.first.title
+ assert_equal t[:type], t.versions.first[:type]
+
+ # make sure that the precision of the price column has been preserved
+ assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
+ assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
+
+ # now lets take 'er back down
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
+ assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
+ end
+ end
+end
--- /dev/null
+ActiveRecord::Schema.define(:version => 0) do
+ create_table :pages, :force => true do |t|
+ t.column :version, :integer
+ t.column :title, :string, :limit => 255
+ t.column :body, :text
+ t.column :updated_on, :datetime
+ t.column :author_id, :integer
+ t.column :revisor_id, :integer
+ end
+
+ create_table :page_versions, :force => true do |t|
+ t.column :page_id, :integer
+ t.column :version, :integer
+ t.column :title, :string, :limit => 255
+ t.column :body, :text
+ t.column :updated_on, :datetime
+ t.column :author_id, :integer
+ t.column :revisor_id, :integer
+ end
+
+ create_table :authors, :force => true do |t|
+ t.column :page_id, :integer
+ t.column :name, :string
+ end
+
+ create_table :locked_pages, :force => true do |t|
+ t.column :lock_version, :integer
+ t.column :title, :string, :limit => 255
+ t.column :type, :string, :limit => 255
+ end
+
+ create_table :locked_pages_revisions, :force => true do |t|
+ t.column :page_id, :integer
+ t.column :version, :integer
+ t.column :title, :string, :limit => 255
+ t.column :version_type, :string, :limit => 255
+ t.column :updated_at, :datetime
+ end
+
+ create_table :widgets, :force => true do |t|
+ t.column :name, :string, :limit => 50
+ t.column :foo, :string
+ t.column :version, :integer
+ t.column :updated_at, :datetime
+ end
+
+ create_table :widget_versions, :force => true do |t|
+ t.column :widget_id, :integer
+ t.column :name, :string, :limit => 50
+ t.column :version, :integer
+ t.column :updated_at, :datetime
+ end
+
+ create_table :landmarks, :force => true do |t|
+ t.column :name, :string
+ t.column :latitude, :float
+ t.column :longitude, :float
+ t.column :version, :integer
+ end
+
+ create_table :landmark_versions, :force => true do |t|
+ t.column :landmark_id, :integer
+ t.column :name, :string
+ t.column :latitude, :float
+ t.column :longitude, :float
+ t.column :version, :integer
+ end
+end
--- /dev/null
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+require File.join(File.dirname(__FILE__), 'fixtures/page')
+require File.join(File.dirname(__FILE__), 'fixtures/widget')
+
+class VersionedTest < Test::Unit::TestCase
+ fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
+ set_fixture_class :page_versions => Page::Version
+
+ def test_saves_versioned_copy
+ p = Page.create! :title => 'first title', :body => 'first body'
+ assert !p.new_record?
+ assert_equal 1, p.versions.size
+ assert_equal 1, p.version
+ assert_instance_of Page.versioned_class, p.versions.first
+ end
+
+ def test_saves_without_revision
+ p = pages(:welcome)
+ old_versions = p.versions.count
+
+ p.save_without_revision
+
+ p.without_revision do
+ p.update_attributes :title => 'changed'
+ end
+
+ assert_equal old_versions, p.versions.count
+ end
+
+ def test_rollback_with_version_number
+ p = pages(:welcome)
+ assert_equal 24, p.version
+ assert_equal 'Welcome to the weblog', p.title
+
+ assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
+ assert_equal 23, p.version
+ assert_equal 'Welcome to the weblg', p.title
+ end
+
+ def test_versioned_class_name
+ assert_equal 'Version', Page.versioned_class_name
+ assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
+ end
+
+ def test_versioned_class
+ assert_equal Page::Version, Page.versioned_class
+ assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
+ end
+
+ def test_special_methods
+ assert_nothing_raised { pages(:welcome).feeling_good? }
+ assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
+ assert_nothing_raised { locked_pages(:welcome).hello_world }
+ assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
+ end
+
+ def test_rollback_with_version_class
+ p = pages(:welcome)
+ assert_equal 24, p.version
+ assert_equal 'Welcome to the weblog', p.title
+
+ assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
+ assert_equal 23, p.version
+ assert_equal 'Welcome to the weblg', p.title
+ end
+
+ def test_rollback_fails_with_invalid_revision
+ p = locked_pages(:welcome)
+ assert !p.revert_to!(locked_pages(:thinking))
+ end
+
+ def test_saves_versioned_copy_with_options
+ p = LockedPage.create! :title => 'first title'
+ assert !p.new_record?
+ assert_equal 1, p.versions.size
+ assert_instance_of LockedPage.versioned_class, p.versions.first
+ end
+
+ def test_rollback_with_version_number_with_options
+ p = locked_pages(:welcome)
+ assert_equal 'Welcome to the weblog', p.title
+ assert_equal 'LockedPage', p.versions.first.version_type
+
+ assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
+ assert_equal 'Welcome to the weblg', p.title
+ assert_equal 'LockedPage', p.versions.first.version_type
+ end
+
+ def test_rollback_with_version_class_with_options
+ p = locked_pages(:welcome)
+ assert_equal 'Welcome to the weblog', p.title
+ assert_equal 'LockedPage', p.versions.first.version_type
+
+ assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
+ assert_equal 'Welcome to the weblg', p.title
+ assert_equal 'LockedPage', p.versions.first.version_type
+ end
+
+ def test_saves_versioned_copy_with_sti
+ p = SpecialLockedPage.create! :title => 'first title'
+ assert !p.new_record?
+ assert_equal 1, p.versions.size
+ assert_instance_of LockedPage.versioned_class, p.versions.first
+ assert_equal 'SpecialLockedPage', p.versions.first.version_type
+ end
+
+ def test_rollback_with_version_number_with_sti
+ p = locked_pages(:thinking)
+ assert_equal 'So I was thinking', p.title
+
+ assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
+ assert_equal 'So I was thinking!!!', p.title
+ assert_equal 'SpecialLockedPage', p.versions.first.version_type
+ end
+
+ def test_lock_version_works_with_versioning
+ p = locked_pages(:thinking)
+ p2 = LockedPage.find(p.id)
+
+ p.title = 'fresh title'
+ p.save
+ assert_equal 2, p.versions.size # limit!
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ p2.title = 'stale title'
+ p2.save
+ end
+ end
+
+ def test_version_if_condition
+ p = Page.create! :title => "title"
+ assert_equal 1, p.version
+
+ Page.feeling_good = false
+ p.save
+ assert_equal 1, p.version
+ Page.feeling_good = true
+ end
+
+ def test_version_if_condition2
+ # set new if condition
+ Page.class_eval do
+ def new_feeling_good() title[0..0] == 'a'; end
+ alias_method :old_feeling_good, :feeling_good?
+ alias_method :feeling_good?, :new_feeling_good
+ end
+
+ p = Page.create! :title => "title"
+ assert_equal 1, p.version # version does not increment
+ assert_equal 1, p.versions(true).size
+
+ p.update_attributes(:title => 'new title')
+ assert_equal 1, p.version # version does not increment
+ assert_equal 1, p.versions(true).size
+
+ p.update_attributes(:title => 'a title')
+ assert_equal 2, p.version
+ assert_equal 2, p.versions(true).size
+
+ # reset original if condition
+ Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
+ end
+
+ def test_version_if_condition_with_block
+ # set new if condition
+ old_condition = Page.version_condition
+ Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
+
+ p = Page.create! :title => "title"
+ assert_equal 1, p.version # version does not increment
+ assert_equal 1, p.versions(true).size
+
+ p.update_attributes(:title => 'a title')
+ assert_equal 1, p.version # version does not increment
+ assert_equal 1, p.versions(true).size
+
+ p.update_attributes(:title => 'b title')
+ assert_equal 2, p.version
+ assert_equal 2, p.versions(true).size
+
+ # reset original if condition
+ Page.version_condition = old_condition
+ end
+
+ def test_version_no_limit
+ p = Page.create! :title => "title", :body => 'first body'
+ p.save
+ p.save
+ 5.times do |i|
+ assert_page_title p, i
+ end
+ end
+
+ def test_version_max_limit
+ p = LockedPage.create! :title => "title"
+ p.update_attributes(:title => "title1")
+ p.update_attributes(:title => "title2")
+ 5.times do |i|
+ assert_page_title p, i, :lock_version
+ assert p.versions(true).size <= 2, "locked version can only store 2 versions"
+ end
+ end
+
+ def test_track_altered_attributes_default_value
+ assert !Page.track_altered_attributes
+ assert LockedPage.track_altered_attributes
+ assert SpecialLockedPage.track_altered_attributes
+ end
+
+ def test_version_order
+ assert_equal 23, pages(:welcome).versions.first.version
+ assert_equal 24, pages(:welcome).versions.last.version
+ end
+
+ def test_track_altered_attributes
+ p = LockedPage.create! :title => "title"
+ assert_equal 1, p.lock_version
+ assert_equal 1, p.versions(true).size
+
+ p.title = 'title'
+ assert !p.save_version?
+ p.save
+ assert_equal 2, p.lock_version # still increments version because of optimistic locking
+ assert_equal 1, p.versions(true).size
+
+ p.title = 'updated title'
+ assert p.save_version?
+ p.save
+ assert_equal 3, p.lock_version
+ assert_equal 1, p.versions(true).size # version 1 deleted
+
+ p.title = 'updated title!'
+ assert p.save_version?
+ p.save
+ assert_equal 4, p.lock_version
+ assert_equal 2, p.versions(true).size # version 1 deleted
+ end
+
+ def assert_page_title(p, i, version_field = :version)
+ p.title = "title#{i}"
+ p.save
+ assert_equal "title#{i}", p.title
+ assert_equal (i+4), p.send(version_field)
+ end
+
+ def test_find_versions
+ assert_equal 2, locked_pages(:welcome).versions.size
+ assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length
+ assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
+ assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
+ assert_equal 2, locked_pages(:welcome).versions.length
+ end
+
+ def test_find_version
+ assert_equal page_versions(:welcome_1), Page.find_version(pages(:welcome).id, 23)
+ assert_equal page_versions(:welcome_2), Page.find_version(pages(:welcome).id, 24)
+ assert_equal pages(:welcome), Page.find_version(pages(:welcome).id)
+
+ assert_equal page_versions(:welcome_1), pages(:welcome).find_version(23)
+ assert_equal page_versions(:welcome_2), pages(:welcome).find_version(24)
+ assert_equal pages(:welcome), pages(:welcome).find_version
+
+ assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(pages(:welcome).id, 1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(0, 23) }
+ end
+
+ def test_with_sequence
+ assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
+ 3.times { Widget.create! :name => 'new widget' }
+ assert_equal 3, Widget.count
+ assert_equal 3, Widget.versioned_class.count
+ end
+
+ def test_has_many_through
+ assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
+ end
+
+ def test_has_many_through_with_custom_association
+ assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
+ end
+
+ def test_referential_integrity
+ pages(:welcome).destroy
+ assert_equal 0, Page.count
+ assert_equal 0, Page::Version.count
+ end
+
+ def test_association_options
+ association = Page.reflect_on_association(:versions)
+ options = association.options
+ assert_equal :delete_all, options[:dependent]
+ assert_equal 'version', options[:order]
+
+ association = Widget.reflect_on_association(:versions)
+ options = association.options
+ assert_equal :nullify, options[:dependent]
+ assert_equal 'version desc', options[:order]
+ assert_equal 'widget_id', options[:foreign_key]
+
+ widget = Widget.create! :name => 'new widget'
+ assert_equal 1, Widget.count
+ assert_equal 1, Widget.versioned_class.count
+ widget.destroy
+ assert_equal 0, Widget.count
+ assert_equal 1, Widget.versioned_class.count
+ end
+
+ def test_versioned_records_should_belong_to_parent
+ page = pages(:welcome)
+ page_version = page.versions.last
+ assert_equal page, page_version.page
+ end
+
+ def test_unaltered_attributes
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
+ assert !landmarks(:washington).changed?
+ end
+
+ def test_unchanged_string_attributes
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
+ assert !landmarks(:washington).changed?
+ end
+
+ def test_should_find_earliest_version
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
+ end
+
+ def test_should_find_latest_version
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
+ end
+
+ def test_should_find_previous_version
+ assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
+ end
+
+ def test_should_find_next_version
+ assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
+ end
+
+ def test_should_find_version_count
+ assert_equal 24, pages(:welcome).versions_count
+ assert_equal 24, page_versions(:welcome_1).versions_count
+ assert_equal 24, page_versions(:welcome_2).versions_count
+ end
+end
\ No newline at end of file
--- /dev/null
+# Include hook code here
+require File.dirname(__FILE__) + '/lib/acts_as_watchable'
+ActiveRecord::Base.send(:include, Redmine::Acts::Watchable)
--- /dev/null
+# 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)
+ send :include, Redmine::Acts::Watchable::InstanceMethods
+
+ class_eval do
+ has_many :watchers, :as => :watchable, :dependent => :delete_all
+ has_many :watcher_users, :through => :watchers, :source => :user
+
+ attr_protected :watcher_ids, :watcher_user_ids
+ end
+ 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
+ self.project.users.sort - self.watcher_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)
+ Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
+ end
+
+ # Adds/removes watcher
+ def set_watcher(user, watching=true)
+ watching ? add_watcher(user) : remove_watcher(user)
+ end
+
+ # Returns if object is watched by user
+ def watched_by?(user)
+ !self.watchers.find(:first,
+ :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
+ end
+
+ # Returns an array of watchers' email addresses
+ def watcher_recipients
+ self.watchers.collect { |w| w.user.mail if w.user.active? }.compact
+ end
+
+ module ClassMethods
+ # Returns the objects that are watched by user
+ def watched_by(user)
+ find(:all,
+ :include => :watchers,
+ :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+Copyright (c) 2007 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+= AwesomeNestedSet
+
+Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer.
+
+== What makes this so awesome?
+
+This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
+
+== Installation
+
+If you are on Rails 2.1 or later:
+
+ script/plugin install git://github.com/collectiveidea/awesome_nested_set.git
+
+== Usage
+
+To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
+
+ class CreateCategories < ActiveRecord::Migration
+ def self.up
+ create_table :categories do |t|
+ t.string :name
+ t.integer :parent_id
+ t.integer :lft
+ t.integer :rgt
+ end
+ end
+
+ def self.down
+ drop_table :categories
+ end
+ end
+
+Enable the nested set functionality by declaring acts_as_nested_set on your model
+
+ class Category < ActiveRecord::Base
+ acts_as_nested_set
+ end
+
+Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info.
+
+== View Helper
+
+The view helper is called #nested_set_options.
+
+Example usage:
+
+ <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
+
+ <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
+
+See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
+
+== References
+
+You can learn more about nested sets at:
+
+ http://www.dbmsmag.com/9603d06.html
+ http://threebit.net/tutorials/nestedset/tutorial1.html
+ http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
+ http://opensource.symetrie.com/trac/better_nested_set/
+
+
+Copyright (c) 2008 Collective Idea, released under the MIT license
\ No newline at end of file
--- /dev/null
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+require 'rcov/rcovtask'
+require "load_multi_rails_rake_tasks"
+
+spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec"))
+PKG_NAME = spec.name
+PKG_VERSION = spec.version
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_zip = true
+ pkg.need_tar = true
+end
+
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the awesome_nested_set plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the awesome_nested_set plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'AwesomeNestedSet'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+namespace :test do
+ desc "just rcov minus html output"
+ Rcov::RcovTask.new(:coverage) do |t|
+ # t.libs << 'test'
+ t.test_files = FileList['test/**/*_test.rb']
+ t.output_dir = 'coverage'
+ t.verbose = true
+ t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
+ end
+end
\ No newline at end of file
--- /dev/null
+Gem::Specification.new do |s|
+ s.name = "awesome_nested_set"
+ s.version = "1.1.1"
+ s.summary = "An awesome replacement for acts_as_nested_set and better_nested_set."
+ s.description = s.summary
+
+ s.files = %w(init.rb MIT-LICENSE Rakefile README.rdoc lib/awesome_nested_set.rb lib/awesome_nested_set/compatability.rb lib/awesome_nested_set/helper.rb lib/awesome_nested_set/named_scope.rb rails/init.rb test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
+
+ s.add_dependency "activerecord", ['>= 1.1']
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = [ "README.rdoc"]
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
+
+ s.test_files = %w(test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
+ s.require_path = 'lib'
+ s.author = "Collective Idea"
+ s.email = "info@collectiveidea.com"
+ s.homepage = "http://collectiveidea.com"
+end
--- /dev/null
+require File.dirname(__FILE__) + "/rails/init"
--- /dev/null
+module CollectiveIdea #:nodoc:
+ module Acts #:nodoc:
+ module NestedSet #:nodoc:
+ def self.included(base)
+ base.extend(SingletonMethods)
+ end
+
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
+ # an _ordered_ tree, with the added feature that you can select the children and all of their
+ # descendants with a single query. The drawback is that insertion or move need some complex
+ # sql queries. But everything is done here by this module!
+ #
+ # Nested sets are appropriate each time you want either an orderd tree (menus,
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
+ #
+ # == API
+ #
+ # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
+ # by another easier, except for the creation:
+ #
+ # in acts_as_tree:
+ # item.children.create(:name => "child1")
+ #
+ # in acts_as_nested_set:
+ # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
+ # child = MyClass.new(:name => "child1")
+ # child.save
+ # # now move the item to its right place
+ # child.move_to_child_of my_item
+ #
+ # You can pass an id or an object to:
+ # * <tt>#move_to_child_of</tt>
+ # * <tt>#move_to_right_of</tt>
+ # * <tt>#move_to_left_of</tt>
+ #
+ module SingletonMethods
+ # Configuration options are:
+ #
+ # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
+ # * +:left_column+ - column name for left boundry data, default "lft"
+ # * +:right_column+ - column name for right boundry data, default "rgt"
+ # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
+ # (if it hasn't been already) and use that as the foreign key restriction. You
+ # can also pass an array to scope by multiple attributes.
+ # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
+ # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
+ # child objects are destroyed alongside this object by calling their destroy
+ # method. If set to :delete_all (default), all the child objects are deleted
+ # without calling their destroy method.
+ #
+ # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
+ # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
+ # to acts_as_nested_set models
+ def acts_as_nested_set(options = {})
+ options = {
+ :parent_column => 'parent_id',
+ :left_column => 'lft',
+ :right_column => 'rgt',
+ :order => 'id',
+ :dependent => :delete_all, # or :destroy
+ }.merge(options)
+
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
+ options[:scope] = "#{options[:scope]}_id".intern
+ end
+
+ write_inheritable_attribute :acts_as_nested_set_options, options
+ class_inheritable_reader :acts_as_nested_set_options
+
+ include Comparable
+ include Columns
+ include InstanceMethods
+ extend Columns
+ extend ClassMethods
+
+ # no bulk assignment
+ attr_protected left_column_name.intern,
+ right_column_name.intern,
+ parent_column_name.intern
+
+ before_create :set_default_left_and_right
+ before_destroy :prune_from_tree
+
+ # no assignment to structure fields
+ [left_column_name, right_column_name, parent_column_name].each do |column|
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{column}=(x)
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
+ end
+ end_eval
+ end
+
+ named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
+ named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
+ if self.respond_to?(:define_callbacks)
+ define_callbacks("before_move", "after_move")
+ end
+
+
+ end
+
+ end
+
+ module ClassMethods
+
+ # Returns the first root
+ def root
+ roots.find(:first)
+ end
+
+ def valid?
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
+ end
+
+ def left_and_rights_valid?
+ count(
+ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
+ "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
+ :conditions =>
+ "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
+ "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
+ "#{quoted_table_name}.#{quoted_left_column_name} >= " +
+ "#{quoted_table_name}.#{quoted_right_column_name} OR " +
+ "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
+ "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
+ "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
+ ) == 0
+ end
+
+ def no_duplicates_for_columns?
+ scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
+ connection.quote_column_name(c)
+ end.push(nil).join(", ")
+ [quoted_left_column_name, quoted_right_column_name].all? do |column|
+ # No duplicates
+ find(:first,
+ :select => "#{scope_string}#{column}, COUNT(#{column})",
+ :group => "#{scope_string}#{column}
+ HAVING COUNT(#{column}) > 1").nil?
+ end
+ end
+
+ # Wrapper for each_root_valid? that can deal with scope.
+ def all_roots_valid?
+ if acts_as_nested_set_options[:scope]
+ roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
+ each_root_valid?(grouped_roots)
+ end
+ else
+ each_root_valid?(roots)
+ end
+ end
+
+ def each_root_valid?(roots_to_validate)
+ left = right = 0
+ roots_to_validate.all? do |root|
+ returning(root.left > left && root.right > right) do
+ left = root.left
+ right = root.right
+ end
+ end
+ end
+
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
+ def rebuild!
+ # Don't rebuild a valid tree.
+ return true if valid?
+
+ scope = lambda{|node|}
+ if acts_as_nested_set_options[:scope]
+ scope = lambda{|node|
+ scope_column_names.inject(""){|str, column_name|
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
+ }
+ }
+ end
+ indices = {}
+
+ set_left_and_rights = lambda do |node|
+ # set left
+ node[left_column_name] = indices[scope.call(node)] += 1
+ # find
+ find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
+ # set right
+ node[right_column_name] = indices[scope.call(node)] += 1
+ node.save!
+ end
+
+ # Find root node(s)
+ root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
+ # setup index for this scope
+ indices[scope.call(root_node)] ||= 0
+ set_left_and_rights.call(root_node)
+ end
+ end
+ end
+
+ # Mixed into both classes and instances to provide easy access to the column names
+ module Columns
+ def left_column_name
+ acts_as_nested_set_options[:left_column]
+ end
+
+ def right_column_name
+ acts_as_nested_set_options[:right_column]
+ end
+
+ def parent_column_name
+ acts_as_nested_set_options[:parent_column]
+ end
+
+ def scope_column_names
+ Array(acts_as_nested_set_options[:scope])
+ end
+
+ def quoted_left_column_name
+ connection.quote_column_name(left_column_name)
+ end
+
+ def quoted_right_column_name
+ connection.quote_column_name(right_column_name)
+ end
+
+ def quoted_parent_column_name
+ connection.quote_column_name(parent_column_name)
+ end
+
+ def quoted_scope_column_names
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
+ end
+ end
+
+ # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
+ #
+ # category.self_and_descendants.count
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
+ module InstanceMethods
+ # Value of the parent column
+ def parent_id
+ self[parent_column_name]
+ end
+
+ # Value of the left column
+ def left
+ self[left_column_name]
+ end
+
+ # Value of the right column
+ def right
+ self[right_column_name]
+ end
+
+ # Returns true if this is a root node.
+ def root?
+ parent_id.nil?
+ end
+
+ def leaf?
+ right - left == 1
+ end
+
+ # Returns true is this is a child node
+ def child?
+ !parent_id.nil?
+ end
+
+ # order by left column
+ def <=>(x)
+ left <=> x.left
+ end
+
+ # Redefine to act like active record
+ def ==(comparison_object)
+ comparison_object.equal?(self) ||
+ (comparison_object.instance_of?(self.class) &&
+ comparison_object.id == id &&
+ !comparison_object.new_record?)
+ end
+
+ # Returns root
+ def root
+ self_and_ancestors.find(:first)
+ end
+
+ # Returns the immediate parent
+ def parent
+ nested_set_scope.find_by_id(parent_id) if parent_id
+ end
+
+ # Returns the array of all parents and self
+ def self_and_ancestors
+ nested_set_scope.scoped :conditions => [
+ "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
+ ]
+ end
+
+ # Returns an array of all parents
+ def ancestors
+ without_self self_and_ancestors
+ end
+
+ # Returns the array of all children of the parent, including self
+ def self_and_siblings
+ nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
+ end
+
+ # Returns the array of all children of the parent, except self
+ def siblings
+ without_self self_and_siblings
+ end
+
+ # Returns a set of all of its nested children which do not have children
+ def leaves
+ descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
+ end
+
+ # Returns the level of this object in the tree
+ # root level is 0
+ def level
+ parent_id.nil? ? 0 : ancestors.count
+ end
+
+ # Returns a set of itself and all of its nested children
+ def self_and_descendants
+ nested_set_scope.scoped :conditions => [
+ "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
+ ]
+ end
+
+ # Returns a set of all of its children and nested children
+ def descendants
+ without_self self_and_descendants
+ end
+
+ # Returns a set of only this entry's immediate children
+ def children
+ nested_set_scope.scoped :conditions => {parent_column_name => self}
+ end
+
+ def is_descendant_of?(other)
+ other.left < self.left && self.left < other.right && same_scope?(other)
+ end
+
+ def is_or_is_descendant_of?(other)
+ other.left <= self.left && self.left < other.right && same_scope?(other)
+ end
+
+ def is_ancestor_of?(other)
+ self.left < other.left && other.left < self.right && same_scope?(other)
+ end
+
+ def is_or_is_ancestor_of?(other)
+ self.left <= other.left && other.left < self.right && same_scope?(other)
+ end
+
+ # Check if other model is in the same scope
+ def same_scope?(other)
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
+ self.send(attr) == other.send(attr)
+ end
+ end
+
+ # Find the first sibling to the left
+ def left_sibling
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
+ :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
+ end
+
+ # Find the first sibling to the right
+ def right_sibling
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
+ end
+
+ # Shorthand method for finding the left sibling and moving to the left of it.
+ def move_left
+ move_to_left_of left_sibling
+ end
+
+ # Shorthand method for finding the right sibling and moving to the right of it.
+ def move_right
+ move_to_right_of right_sibling
+ end
+
+ # Move the node to the left of another node (you can pass id only)
+ def move_to_left_of(node)
+ move_to node, :left
+ end
+
+ # Move the node to the left of another node (you can pass id only)
+ def move_to_right_of(node)
+ move_to node, :right
+ end
+
+ # Move the node to the child of another node (you can pass id only)
+ def move_to_child_of(node)
+ move_to node, :child
+ end
+
+ # Move the node to root nodes
+ def move_to_root
+ move_to nil, :root
+ end
+
+ def move_possible?(target)
+ self != target && # Can't target self
+ same_scope?(target) && # can't be in different scopes
+ # !(left..right).include?(target.left..target.right) # this needs tested more
+ # detect impossible move
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
+ end
+
+ def to_text
+ self_and_descendants.map do |node|
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
+ end.join("\n")
+ end
+
+ protected
+
+ def without_self(scope)
+ scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
+ end
+
+ # All nested set queries should use this nested_set_scope, which performs finds on
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
+ # declaration.
+ def nested_set_scope
+ options = {:order => quoted_left_column_name}
+ scopes = Array(acts_as_nested_set_options[:scope])
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
+ conditions.merge attr => self[attr]
+ end unless scopes.empty?
+ self.class.base_class.scoped options
+ end
+
+ # on creation, set automatically lft and rgt to the end of the tree
+ def set_default_left_and_right
+ maxright = nested_set_scope.maximum(right_column_name) || 0
+ # adds the new node to the right of all existing nodes
+ self[left_column_name] = maxright + 1
+ self[right_column_name] = maxright + 2
+ end
+
+ # Prunes a branch off of the tree, shifting all of the elements on the right
+ # back to the left so the counts still work.
+ def prune_from_tree
+ return if right.nil? || left.nil?
+ diff = right - left + 1
+
+ delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
+ :destroy_all : :delete_all
+
+ self.class.base_class.transaction do
+ nested_set_scope.send(delete_method,
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
+ left, right]
+ )
+ nested_set_scope.update_all(
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
+ ["#{quoted_left_column_name} >= ?", right]
+ )
+ nested_set_scope.update_all(
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
+ ["#{quoted_right_column_name} >= ?", right]
+ )
+ end
+ end
+
+ # reload left, right, and parent
+ def reload_nested_set
+ reload(:select => "#{quoted_left_column_name}, " +
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
+ end
+
+ def move_to(target, position)
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
+ return if callback(:before_move) == false
+ transaction do
+ if target.is_a? self.class.base_class
+ target.reload_nested_set
+ elsif position != :root
+ # load object if node is not an object
+ target = nested_set_scope.find(target)
+ end
+ self.reload_nested_set
+
+ unless position == :root || move_possible?(target)
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
+ end
+
+ bound = case position
+ when :child; target[right_column_name]
+ when :left; target[left_column_name]
+ when :right; target[right_column_name] + 1
+ when :root; 1
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
+ end
+
+ if bound > self[right_column_name]
+ bound = bound - 1
+ other_bound = self[right_column_name] + 1
+ else
+ other_bound = self[left_column_name] - 1
+ end
+
+ # there would be no change
+ return if bound == self[right_column_name] || bound == self[left_column_name]
+
+ # we have defined the boundaries of two non-overlapping intervals,
+ # so sorting puts both the intervals and their boundaries in order
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
+
+ new_parent = case position
+ when :child; target.id
+ when :root; nil
+ else target[parent_column_name]
+ end
+
+ self.class.base_class.update_all([
+ "#{quoted_left_column_name} = CASE " +
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
+ "THEN #{quoted_left_column_name} + :d - :b " +
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
+ "THEN #{quoted_left_column_name} + :a - :c " +
+ "ELSE #{quoted_left_column_name} END, " +
+ "#{quoted_right_column_name} = CASE " +
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
+ "THEN #{quoted_right_column_name} + :d - :b " +
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
+ "THEN #{quoted_right_column_name} + :a - :c " +
+ "ELSE #{quoted_right_column_name} END, " +
+ "#{quoted_parent_column_name} = CASE " +
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
+ "ELSE #{quoted_parent_column_name} END",
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
+ ], nested_set_scope.proxy_options[:conditions])
+ end
+ target.reload_nested_set if target
+ self.reload_nested_set
+ callback(:after_move)
+ end
+
+ end
+
+ end
+ end
+end
--- /dev/null
+# Rails <2.x doesn't define #except
+class Hash #:nodoc:
+ # Returns a new hash without the given keys.
+ def except(*keys)
+ clone.except!(*keys)
+ end unless method_defined?(:except)
+
+ # Replaces the hash without the given keys.
+ def except!(*keys)
+ keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
+ keys.each { |key| delete(key) }
+ self
+ end unless method_defined?(:except!)
+end
+
+# NamedScope is new to Rails 2.1
+unless defined? ActiveRecord::NamedScope
+ require 'awesome_nested_set/named_scope'
+ ActiveRecord::Base.class_eval do
+ include CollectiveIdea::NamedScope
+ end
+end
+
+# Rails 1.2.x doesn't define #quoted_table_name
+class ActiveRecord::Base #:nodoc:
+ def self.quoted_table_name
+ self.connection.quote_column_name(self.table_name)
+ end unless methods.include?('quoted_table_name')
+end
\ No newline at end of file
--- /dev/null
+module CollectiveIdea #:nodoc:
+ module Acts #:nodoc:
+ module NestedSet #:nodoc:
+ # This module provides some helpers for the model classes using acts_as_nested_set.
+ # It is included by default in all views.
+ #
+ module Helper
+ # Returns options for select.
+ # You can exclude some items from the tree.
+ # You can pass a block receiving an item and returning the string displayed in the select.
+ #
+ # == Params
+ # * +class_or_item+ - Class name or top level times
+ # * +mover+ - The item that is being move, used to exlude impossible moves
+ # * +&block+ - a block that will be used to display: { |item| ... item.name }
+ #
+ # == Usage
+ #
+ # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
+ # "#{'–' * i.level} #{i.name}"
+ # }) %>
+ #
+ def nested_set_options(class_or_item, mover = nil)
+ class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
+ items = Array(class_or_item)
+ result = []
+ items.each do |root|
+ result += root.self_and_descendants.map do |i|
+ if mover.nil? || mover.new_record? || mover.move_possible?(i)
+ [yield(i), i.id]
+ end
+ end.compact
+ end
+ result
+ end
+
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+# Taken from Rails 2.1
+module CollectiveIdea #:nodoc:
+ module NamedScope #:nodoc:
+ # All subclasses of ActiveRecord::Base have two named_scopes:
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
+ #
+ # Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
+ #
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
+ # intermediate values (scopes) around as first-class objects is convenient.
+ def self.included(base)
+ base.class_eval do
+ extend ClassMethods
+ named_scope :scoped, lambda { |scope| scope }
+ end
+ end
+
+ module ClassMethods #:nodoc:
+ def scopes
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
+ end
+
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :red, :conditions => {:color => 'red'}
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
+ # end
+ #
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
+ #
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
+ #
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
+ #
+ # All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
+ # <tt>has_many</tt> associations. If,
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :shirts
+ # end
+ #
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
+ # only shirts.
+ #
+ # Named scopes can also be procedural.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :colored, lambda { |color|
+ # { :conditions => { :color => color } }
+ # }
+ # end
+ #
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
+ #
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :red, :conditions => {:color => 'red'} do
+ # def dom_id
+ # 'red_shirts'
+ # end
+ # end
+ # end
+ #
+ #
+ # For testing complex named scopes, you can examine the scoping options using the
+ # <tt>proxy_options</tt> method on the proxy itself.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :colored, lambda { |color|
+ # { :conditions => { :color => color } }
+ # }
+ # end
+ #
+ # expected_options = { :conditions => { :colored => 'red' } }
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
+ def named_scope(name, options = {}, &block)
+ scopes[name] = lambda do |parent_scope, *args|
+ Scope.new(parent_scope, case options
+ when Hash
+ options
+ when Proc
+ options.call(*args)
+ end, &block)
+ end
+ (class << self; self end).instance_eval do
+ define_method name do |*args|
+ scopes[name].call(self, *args)
+ end
+ end
+ end
+ end
+
+ class Scope #:nodoc:
+ attr_reader :proxy_scope, :proxy_options
+ [].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
+ delegate :scopes, :with_scope, :to => :proxy_scope
+
+ def initialize(proxy_scope, options, &block)
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
+ extend Module.new(&block) if block_given?
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
+ end
+
+ def reload
+ load_found; self
+ end
+
+ protected
+ def proxy_found
+ @found || load_found
+ end
+
+ private
+ def method_missing(method, *args, &block)
+ if scopes.include?(method)
+ scopes[method].call(self, *args)
+ else
+ with_scope :find => proxy_options do
+ proxy_scope.send(method, *args, &block)
+ end
+ end
+ end
+
+ def load_found
+ @found = find(:all)
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+require 'awesome_nested_set/compatability'
+require 'awesome_nested_set'
+
+ActiveRecord::Base.class_eval do
+ include CollectiveIdea::Acts::NestedSet
+end
+
+if defined?(ActionView)
+ require 'awesome_nested_set/helper'
+ ActionView::Base.class_eval do
+ include CollectiveIdea::Acts::NestedSet::Helper
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+module CollectiveIdea
+ module Acts #:nodoc:
+ module NestedSet #:nodoc:
+ class AwesomeNestedSetTest < Test::Unit::TestCase
+ include Helper
+ fixtures :categories
+
+ def test_nested_set_options
+ expected = [
+ [" Top Level", 1],
+ ["- Child 1", 2],
+ ['- Child 2', 3],
+ ['-- Child 2.1', 4],
+ ['- Child 3', 5],
+ [" Top Level 2", 6]
+ ]
+ actual = nested_set_options(Category) do |c|
+ "#{'-' * c.level} #{c.name}"
+ end
+ assert_equal expected, actual
+ end
+
+ def test_nested_set_options_with_mover
+ expected = [
+ [" Top Level", 1],
+ ["- Child 1", 2],
+ ['- Child 3', 5],
+ [" Top Level 2", 6]
+ ]
+ actual = nested_set_options(Category, categories(:child_2)) do |c|
+ "#{'-' * c.level} #{c.name}"
+ end
+ assert_equal expected, actual
+ end
+
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/test_helper'
+
+class Note < ActiveRecord::Base
+ acts_as_nested_set :scope => [:notable_id, :notable_type]
+end
+
+class AwesomeNestedSetTest < Test::Unit::TestCase
+
+ class Default < ActiveRecord::Base
+ acts_as_nested_set
+ set_table_name 'categories'
+ end
+ class Scoped < ActiveRecord::Base
+ acts_as_nested_set :scope => :organization
+ set_table_name 'categories'
+ end
+
+ def test_left_column_default
+ assert_equal 'lft', Default.acts_as_nested_set_options[:left_column]
+ end
+
+ def test_right_column_default
+ assert_equal 'rgt', Default.acts_as_nested_set_options[:right_column]
+ end
+
+ def test_parent_column_default
+ assert_equal 'parent_id', Default.acts_as_nested_set_options[:parent_column]
+ end
+
+ def test_scope_default
+ assert_nil Default.acts_as_nested_set_options[:scope]
+ end
+
+ def test_left_column_name
+ assert_equal 'lft', Default.left_column_name
+ assert_equal 'lft', Default.new.left_column_name
+ end
+
+ def test_right_column_name
+ assert_equal 'rgt', Default.right_column_name
+ assert_equal 'rgt', Default.new.right_column_name
+ end
+
+ def test_parent_column_name
+ assert_equal 'parent_id', Default.parent_column_name
+ assert_equal 'parent_id', Default.new.parent_column_name
+ end
+
+ def test_quoted_left_column_name
+ quoted = Default.connection.quote_column_name('lft')
+ assert_equal quoted, Default.quoted_left_column_name
+ assert_equal quoted, Default.new.quoted_left_column_name
+ end
+
+ def test_quoted_right_column_name
+ quoted = Default.connection.quote_column_name('rgt')
+ assert_equal quoted, Default.quoted_right_column_name
+ assert_equal quoted, Default.new.quoted_right_column_name
+ end
+
+ def test_left_column_protected_from_assignment
+ assert_raises(ActiveRecord::ActiveRecordError) { Category.new.lft = 1 }
+ end
+
+ def test_right_column_protected_from_assignment
+ assert_raises(ActiveRecord::ActiveRecordError) { Category.new.rgt = 1 }
+ end
+
+ def test_parent_column_protected_from_assignment
+ assert_raises(ActiveRecord::ActiveRecordError) { Category.new.parent_id = 1 }
+ end
+
+ def test_colums_protected_on_initialize
+ c = Category.new(:lft => 1, :rgt => 2, :parent_id => 3)
+ assert_nil c.lft
+ assert_nil c.rgt
+ assert_nil c.parent_id
+ end
+
+ def test_scoped_appends_id
+ assert_equal :organization_id, Scoped.acts_as_nested_set_options[:scope]
+ end
+
+ def test_roots_class_method
+ assert_equal Category.find_all_by_parent_id(nil), Category.roots
+ end
+
+ def test_root_class_method
+ assert_equal categories(:top_level), Category.root
+ end
+
+ def test_root
+ assert_equal categories(:top_level), categories(:child_3).root
+ end
+
+ def test_root?
+ assert categories(:top_level).root?
+ assert categories(:top_level_2).root?
+ end
+
+ def test_leaves_class_method
+ assert_equal Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1"), Category.leaves
+ assert_equal Category.leaves.count, 4
+ assert (Category.leaves.include? categories(:child_1))
+ assert (Category.leaves.include? categories(:child_2_1))
+ assert (Category.leaves.include? categories(:child_3))
+ assert (Category.leaves.include? categories(:top_level_2))
+ end
+
+ def test_leaf
+ assert categories(:child_1).leaf?
+ assert categories(:child_2_1).leaf?
+ assert categories(:child_3).leaf?
+ assert categories(:top_level_2).leaf?
+
+ assert !categories(:top_level).leaf?
+ assert !categories(:child_2).leaf?
+ end
+
+ def test_parent
+ assert_equal categories(:child_2), categories(:child_2_1).parent
+ end
+
+ def test_self_and_ancestors
+ child = categories(:child_2_1)
+ self_and_ancestors = [categories(:top_level), categories(:child_2), child]
+ assert_equal self_and_ancestors, child.self_and_ancestors
+ end
+
+ def test_ancestors
+ child = categories(:child_2_1)
+ ancestors = [categories(:top_level), categories(:child_2)]
+ assert_equal ancestors, child.ancestors
+ end
+
+ def test_self_and_siblings
+ child = categories(:child_2)
+ self_and_siblings = [categories(:child_1), child, categories(:child_3)]
+ assert_equal self_and_siblings, child.self_and_siblings
+ assert_nothing_raised do
+ tops = [categories(:top_level), categories(:top_level_2)]
+ assert_equal tops, categories(:top_level).self_and_siblings
+ end
+ end
+
+ def test_siblings
+ child = categories(:child_2)
+ siblings = [categories(:child_1), categories(:child_3)]
+ assert_equal siblings, child.siblings
+ end
+
+ def test_leaves
+ leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3), categories(:top_level_2)]
+ assert categories(:top_level).leaves, leaves
+ end
+
+ def test_level
+ assert_equal 0, categories(:top_level).level
+ assert_equal 1, categories(:child_1).level
+ assert_equal 2, categories(:child_2_1).level
+ end
+
+ def test_has_children?
+ assert categories(:child_2_1).children.empty?
+ assert !categories(:child_2).children.empty?
+ assert !categories(:top_level).children.empty?
+ end
+
+ def test_self_and_descendents
+ parent = categories(:top_level)
+ self_and_descendants = [parent, categories(:child_1), categories(:child_2),
+ categories(:child_2_1), categories(:child_3)]
+ assert_equal self_and_descendants, parent.self_and_descendants
+ assert_equal self_and_descendants, parent.self_and_descendants.count
+ end
+
+ def test_descendents
+ lawyers = Category.create!(:name => "lawyers")
+ us = Category.create!(:name => "United States")
+ us.move_to_child_of(lawyers)
+ patent = Category.create!(:name => "Patent Law")
+ patent.move_to_child_of(us)
+ lawyers.reload
+
+ assert_equal 1, lawyers.children.size
+ assert_equal 1, us.children.size
+ assert_equal 2, lawyers.descendants.size
+ end
+
+ def test_self_and_descendents
+ parent = categories(:top_level)
+ descendants = [categories(:child_1), categories(:child_2),
+ categories(:child_2_1), categories(:child_3)]
+ assert_equal descendants, parent.descendants
+ end
+
+ def test_children
+ category = categories(:top_level)
+ category.children.each {|c| assert_equal category.id, c.parent_id }
+ end
+
+ def test_is_or_is_ancestor_of?
+ assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_1))
+ assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1))
+ assert categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1))
+ assert !categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2))
+ assert !categories(:child_1).is_or_is_ancestor_of?(categories(:child_2))
+ assert categories(:child_1).is_or_is_ancestor_of?(categories(:child_1))
+ end
+
+ def test_is_ancestor_of?
+ assert categories(:top_level).is_ancestor_of?(categories(:child_1))
+ assert categories(:top_level).is_ancestor_of?(categories(:child_2_1))
+ assert categories(:child_2).is_ancestor_of?(categories(:child_2_1))
+ assert !categories(:child_2_1).is_ancestor_of?(categories(:child_2))
+ assert !categories(:child_1).is_ancestor_of?(categories(:child_2))
+ assert !categories(:child_1).is_ancestor_of?(categories(:child_1))
+ end
+
+ def test_is_or_is_ancestor_of_with_scope
+ root = Scoped.root
+ child = root.children.first
+ assert root.is_or_is_ancestor_of?(child)
+ child.update_attribute :organization_id, 'different'
+ assert !root.is_or_is_ancestor_of?(child)
+ end
+
+ def test_is_or_is_descendant_of?
+ assert categories(:child_1).is_or_is_descendant_of?(categories(:top_level))
+ assert categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level))
+ assert categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2))
+ assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1))
+ assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_1))
+ assert categories(:child_1).is_or_is_descendant_of?(categories(:child_1))
+ end
+
+ def test_is_descendant_of?
+ assert categories(:child_1).is_descendant_of?(categories(:top_level))
+ assert categories(:child_2_1).is_descendant_of?(categories(:top_level))
+ assert categories(:child_2_1).is_descendant_of?(categories(:child_2))
+ assert !categories(:child_2).is_descendant_of?(categories(:child_2_1))
+ assert !categories(:child_2).is_descendant_of?(categories(:child_1))
+ assert !categories(:child_1).is_descendant_of?(categories(:child_1))
+ end
+
+ def test_is_or_is_descendant_of_with_scope
+ root = Scoped.root
+ child = root.children.first
+ assert child.is_or_is_descendant_of?(root)
+ child.update_attribute :organization_id, 'different'
+ assert !child.is_or_is_descendant_of?(root)
+ end
+
+ def test_same_scope?
+ root = Scoped.root
+ child = root.children.first
+ assert child.same_scope?(root)
+ child.update_attribute :organization_id, 'different'
+ assert !child.same_scope?(root)
+ end
+
+ def test_left_sibling
+ assert_equal categories(:child_1), categories(:child_2).left_sibling
+ assert_equal categories(:child_2), categories(:child_3).left_sibling
+ end
+
+ def test_left_sibling_of_root
+ assert_nil categories(:top_level).left_sibling
+ end
+
+ def test_left_sibling_without_siblings
+ assert_nil categories(:child_2_1).left_sibling
+ end
+
+ def test_left_sibling_of_leftmost_node
+ assert_nil categories(:child_1).left_sibling
+ end
+
+ def test_right_sibling
+ assert_equal categories(:child_3), categories(:child_2).right_sibling
+ assert_equal categories(:child_2), categories(:child_1).right_sibling
+ end
+
+ def test_right_sibling_of_root
+ assert_equal categories(:top_level_2), categories(:top_level).right_sibling
+ assert_nil categories(:top_level_2).right_sibling
+ end
+
+ def test_right_sibling_without_siblings
+ assert_nil categories(:child_2_1).right_sibling
+ end
+
+ def test_right_sibling_of_rightmost_node
+ assert_nil categories(:child_3).right_sibling
+ end
+
+ def test_move_left
+ categories(:child_2).move_left
+ assert_nil categories(:child_2).left_sibling
+ assert_equal categories(:child_1), categories(:child_2).right_sibling
+ assert Category.valid?
+ end
+
+ def test_move_right
+ categories(:child_2).move_right
+ assert_nil categories(:child_2).right_sibling
+ assert_equal categories(:child_3), categories(:child_2).left_sibling
+ assert Category.valid?
+ end
+
+ def test_move_to_left_of
+ categories(:child_3).move_to_left_of(categories(:child_1))
+ assert_nil categories(:child_3).left_sibling
+ assert_equal categories(:child_1), categories(:child_3).right_sibling
+ assert Category.valid?
+ end
+
+ def test_move_to_right_of
+ categories(:child_1).move_to_right_of(categories(:child_3))
+ assert_nil categories(:child_1).right_sibling
+ assert_equal categories(:child_3), categories(:child_1).left_sibling
+ assert Category.valid?
+ end
+
+ def test_move_to_root
+ categories(:child_2).move_to_root
+ assert_nil categories(:child_2).parent
+ assert_equal 0, categories(:child_2).level
+ assert_equal 1, categories(:child_2_1).level
+ assert_equal 1, categories(:child_2).left
+ assert_equal 4, categories(:child_2).right
+ assert Category.valid?
+ end
+
+ def test_move_to_child_of
+ categories(:child_1).move_to_child_of(categories(:child_3))
+ assert_equal categories(:child_3).id, categories(:child_1).parent_id
+ assert Category.valid?
+ end
+
+ def test_move_to_child_of_appends_to_end
+ child = Category.create! :name => 'New Child'
+ child.move_to_child_of categories(:top_level)
+ assert_equal child, categories(:top_level).children.last
+ end
+
+ def test_subtree_move_to_child_of
+ assert_equal 4, categories(:child_2).left
+ assert_equal 7, categories(:child_2).right
+
+ assert_equal 2, categories(:child_1).left
+ assert_equal 3, categories(:child_1).right
+
+ categories(:child_2).move_to_child_of(categories(:child_1))
+ assert Category.valid?
+ assert_equal categories(:child_1).id, categories(:child_2).parent_id
+
+ assert_equal 3, categories(:child_2).left
+ assert_equal 6, categories(:child_2).right
+ assert_equal 2, categories(:child_1).left
+ assert_equal 7, categories(:child_1).right
+ end
+
+ def test_slightly_difficult_move_to_child_of
+ assert_equal 11, categories(:top_level_2).left
+ assert_equal 12, categories(:top_level_2).right
+
+ # create a new top-level node and move single-node top-level tree inside it.
+ new_top = Category.create(:name => 'New Top')
+ assert_equal 13, new_top.left
+ assert_equal 14, new_top.right
+
+ categories(:top_level_2).move_to_child_of(new_top)
+
+ assert Category.valid?
+ assert_equal new_top.id, categories(:top_level_2).parent_id
+
+ assert_equal 12, categories(:top_level_2).left
+ assert_equal 13, categories(:top_level_2).right
+ assert_equal 11, new_top.left
+ assert_equal 14, new_top.right
+ end
+
+ def test_difficult_move_to_child_of
+ assert_equal 1, categories(:top_level).left
+ assert_equal 10, categories(:top_level).right
+ assert_equal 5, categories(:child_2_1).left
+ assert_equal 6, categories(:child_2_1).right
+
+ # create a new top-level node and move an entire top-level tree inside it.
+ new_top = Category.create(:name => 'New Top')
+ categories(:top_level).move_to_child_of(new_top)
+ categories(:child_2_1).reload
+ assert Category.valid?
+ assert_equal new_top.id, categories(:top_level).parent_id
+
+ assert_equal 4, categories(:top_level).left
+ assert_equal 13, categories(:top_level).right
+ assert_equal 8, categories(:child_2_1).left
+ assert_equal 9, categories(:child_2_1).right
+ end
+
+ #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
+ def test_move_to_child_more_than_once_per_parent_rebuild
+ root1 = Category.create(:name => 'Root1')
+ root2 = Category.create(:name => 'Root2')
+ root3 = Category.create(:name => 'Root3')
+
+ root2.move_to_child_of root1
+ root3.move_to_child_of root1
+
+ output = Category.roots.last.to_text
+ Category.update_all('lft = null, rgt = null')
+ Category.rebuild!
+
+ assert_equal Category.roots.last.to_text, output
+ end
+
+ # doing move_to_child twice onto same parent from the furthest right first
+ def test_move_to_child_more_than_once_per_parent_outside_in
+ node1 = Category.create(:name => 'Node-1')
+ node2 = Category.create(:name => 'Node-2')
+ node3 = Category.create(:name => 'Node-3')
+
+ node2.move_to_child_of node1
+ node3.move_to_child_of node1
+
+ output = Category.roots.last.to_text
+ Category.update_all('lft = null, rgt = null')
+ Category.rebuild!
+
+ assert_equal Category.roots.last.to_text, output
+ end
+
+
+ def test_valid_with_null_lefts
+ assert Category.valid?
+ Category.update_all('lft = null')
+ assert !Category.valid?
+ end
+
+ def test_valid_with_null_rights
+ assert Category.valid?
+ Category.update_all('rgt = null')
+ assert !Category.valid?
+ end
+
+ def test_valid_with_missing_intermediate_node
+ # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
+ assert Category.valid?
+ Category.delete(categories(:child_2).id)
+ assert Category.valid?
+ end
+
+ def test_valid_with_overlapping_and_rights
+ assert Category.valid?
+ categories(:top_level_2)['lft'] = 0
+ categories(:top_level_2).save
+ assert !Category.valid?
+ end
+
+ def test_rebuild
+ assert Category.valid?
+ before_text = Category.root.to_text
+ Category.update_all('lft = null, rgt = null')
+ Category.rebuild!
+ assert Category.valid?
+ assert_equal before_text, Category.root.to_text
+ end
+
+ def test_move_possible_for_sibling
+ assert categories(:child_2).move_possible?(categories(:child_1))
+ end
+
+ def test_move_not_possible_to_self
+ assert !categories(:top_level).move_possible?(categories(:top_level))
+ end
+
+ def test_move_not_possible_to_parent
+ categories(:top_level).descendants.each do |descendant|
+ assert !categories(:top_level).move_possible?(descendant)
+ assert descendant.move_possible?(categories(:top_level))
+ end
+ end
+
+ def test_is_or_is_ancestor_of?
+ [:child_1, :child_2, :child_2_1, :child_3].each do |c|
+ assert categories(:top_level).is_or_is_ancestor_of?(categories(c))
+ end
+ assert !categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2))
+ end
+
+ def test_left_and_rights_valid_with_blank_left
+ assert Category.left_and_rights_valid?
+ categories(:child_2)[:lft] = nil
+ categories(:child_2).save(false)
+ assert !Category.left_and_rights_valid?
+ end
+
+ def test_left_and_rights_valid_with_blank_right
+ assert Category.left_and_rights_valid?
+ categories(:child_2)[:rgt] = nil
+ categories(:child_2).save(false)
+ assert !Category.left_and_rights_valid?
+ end
+
+ def test_left_and_rights_valid_with_equal
+ assert Category.left_and_rights_valid?
+ categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
+ categories(:top_level_2).save(false)
+ assert !Category.left_and_rights_valid?
+ end
+
+ def test_left_and_rights_valid_with_left_equal_to_parent
+ assert Category.left_and_rights_valid?
+ categories(:child_2)[:lft] = categories(:top_level)[:lft]
+ categories(:child_2).save(false)
+ assert !Category.left_and_rights_valid?
+ end
+
+ def test_left_and_rights_valid_with_right_equal_to_parent
+ assert Category.left_and_rights_valid?
+ categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
+ categories(:child_2).save(false)
+ assert !Category.left_and_rights_valid?
+ end
+
+ def test_moving_dirty_objects_doesnt_invalidate_tree
+ r1 = Category.create
+ r2 = Category.create
+ r3 = Category.create
+ r4 = Category.create
+ nodes = [r1, r2, r3, r4]
+
+ r2.move_to_child_of(r1)
+ assert Category.valid?
+
+ r3.move_to_child_of(r1)
+ assert Category.valid?
+
+ r4.move_to_child_of(r2)
+ assert Category.valid?
+ end
+
+ def test_multi_scoped_no_duplicates_for_columns?
+ assert_nothing_raised do
+ Note.no_duplicates_for_columns?
+ end
+ end
+
+ def test_multi_scoped_all_roots_valid?
+ assert_nothing_raised do
+ Note.all_roots_valid?
+ end
+ end
+
+ def test_multi_scoped
+ note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
+ note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
+ note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
+
+ assert_equal [note1, note2], note1.self_and_siblings
+ assert_equal [note3], note3.self_and_siblings
+ end
+
+ def test_multi_scoped_rebuild
+ root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
+ child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
+ child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
+
+ child1.move_to_child_of root
+ child2.move_to_child_of root
+
+ Note.update_all('lft = null, rgt = null')
+ Note.rebuild!
+
+ assert_equal Note.roots.find_by_body('A'), root
+ assert_equal [child1, child2], Note.roots.find_by_body('A').children
+ end
+
+ def test_same_scope_with_multi_scopes
+ assert_nothing_raised do
+ notes(:scope1).same_scope?(notes(:child_1))
+ end
+ assert notes(:scope1).same_scope?(notes(:child_1))
+ assert notes(:child_1).same_scope?(notes(:scope1))
+ assert !notes(:scope1).same_scope?(notes(:scope2))
+ end
+
+ def test_quoting_of_multi_scope_column_names
+ assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
+ end
+
+ def test_equal_in_same_scope
+ assert_equal notes(:scope1), notes(:scope1)
+ assert_not_equal notes(:scope1), notes(:child_1)
+ end
+
+ def test_equal_in_different_scopes
+ assert_not_equal notes(:scope1), notes(:scope2)
+ end
+
+end
--- /dev/null
+sqlite3:
+ adapter: sqlite3
+ dbfile: awesome_nested_set.sqlite3.db
+sqlite3mem:
+ :adapter: sqlite3
+ :dbfile: ":memory:"
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: awesome_nested_set_plugin_test
+ :min_messages: ERROR
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: root
+ :password:
+ :database: awesome_nested_set_plugin_test
\ No newline at end of file
--- /dev/null
+ActiveRecord::Schema.define(:version => 0) do
+
+ create_table :categories, :force => true do |t|
+ t.column :name, :string
+ t.column :parent_id, :integer
+ t.column :lft, :integer
+ t.column :rgt, :integer
+ t.column :organization_id, :integer
+ end
+
+ create_table :departments, :force => true do |t|
+ t.column :name, :string
+ end
+
+ create_table :notes, :force => true do |t|
+ t.column :body, :text
+ t.column :parent_id, :integer
+ t.column :lft, :integer
+ t.column :rgt, :integer
+ t.column :notable_id, :integer
+ t.column :notable_type, :string
+ end
+end
--- /dev/null
+top_level:
+ id: 1
+ name: Top Level
+ lft: 1
+ rgt: 10
+child_1:
+ id: 2
+ name: Child 1
+ parent_id: 1
+ lft: 2
+ rgt: 3
+child_2:
+ id: 3
+ name: Child 2
+ parent_id: 1
+ lft: 4
+ rgt: 7
+child_2_1:
+ id: 4
+ name: Child 2.1
+ parent_id: 3
+ lft: 5
+ rgt: 6
+child_3:
+ id: 5
+ name: Child 3
+ parent_id: 1
+ lft: 8
+ rgt: 9
+top_level_2:
+ id: 6
+ name: Top Level 2
+ lft: 11
+ rgt: 12
--- /dev/null
+class Category < ActiveRecord::Base
+ acts_as_nested_set
+
+ def to_s
+ name
+ end
+
+ def recurse &block
+ block.call self, lambda{
+ self.children.each do |child|
+ child.recurse &block
+ end
+ }
+ end
+end
\ No newline at end of file
--- /dev/null
+top:
+ id: 1
+ name: Top
\ No newline at end of file
--- /dev/null
+scope1:
+ id: 1
+ body: Top Level
+ lft: 1
+ rgt: 10
+ notable_id: 1
+ notable_type: Category
+child_1:
+ id: 2
+ body: Child 1
+ parent_id: 1
+ lft: 2
+ rgt: 3
+ notable_id: 1
+ notable_type: Category
+child_2:
+ id: 3
+ body: Child 2
+ parent_id: 1
+ lft: 4
+ rgt: 7
+ notable_id: 1
+ notable_type: Category
+child_3:
+ id: 4
+ body: Child 3
+ parent_id: 1
+ lft: 8
+ rgt: 9
+ notable_id: 1
+ notable_type: Category
+scope2:
+ id: 5
+ body: Top Level 2
+ lft: 1
+ rgt: 2
+ notable_id: 1
+ notable_type: Departments
--- /dev/null
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+plugin_test_dir = File.dirname(__FILE__)
+
+require 'rubygems'
+require 'test/unit'
+require 'multi_rails_init'
+# gem 'activerecord', '>= 2.0'
+require 'active_record'
+require 'action_controller'
+require 'action_view'
+require 'active_record/fixtures'
+
+require plugin_test_dir + '/../init.rb'
+
+ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
+
+ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
+ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
+ActiveRecord::Migration.verbose = false
+load(File.join(plugin_test_dir, "db", "schema.rb"))
+
+Dir["#{plugin_test_dir}/fixtures/*.rb"].each {|file| require file }
+
+
+class Test::Unit::TestCase #:nodoc:
+ self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+ self.use_transactional_fixtures = true
+ self.use_instantiated_fixtures = false
+
+ fixtures :categories, :notes, :departments
+end
\ No newline at end of file
--- /dev/null
+* Exported the changelog of Pagination code for historical reference.
+
+* Imported some patches from Rails Trac (others closed as "wontfix"):
+ #8176, #7325, #7028, #4113. Documentation is much cleaner now and there
+ are some new unobtrusive features!
+
+* Extracted Pagination from Rails trunk (r6795)
+
+#
+# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb
+#
+# Generated by Trac 0.10.3
+# 05/20/07 23:48:02
+#
+
+09/03/06 23:28:54 david [4953]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Docs and deprecation
+
+08/07/06 12:40:14 bitsweat [4715]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Deprecate direct usage of @params. Update ActionView::Base for
+ instance var deprecation.
+
+06/21/06 02:16:11 rick [4476]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Fix indent in pagination documentation. Closes #4990. [Kevin Clark]
+
+04/25/06 17:42:48 marcel [4268]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Remove all remaining references to @params in the documentation.
+
+03/16/06 06:38:08 rick [3899]
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ trivial documentation patch for #pagination_links [Francois
+ Beausoleil] closes #4258
+
+02/20/06 03:15:22 david [3620]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/test/activerecord/pagination_test.rb (modified)
+ * trunk/activerecord/CHANGELOG (modified)
+ * trunk/activerecord/lib/active_record/base.rb (modified)
+ * trunk/activerecord/test/base_test.rb (modified)
+ Added :count option to pagination that'll make it possible for the
+ ActiveRecord::Base.count call to using something else than * for the
+ count. Especially important for count queries using DISTINCT #3839
+ [skaes]. Added :select option to Base.count that'll allow you to
+ select something else than * to be counted on. Especially important
+ for count queries using DISTINCT (closes #3839) [skaes].
+
+02/09/06 09:17:40 nzkoz [3553]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/test/active_record_unit.rb (added)
+ * trunk/actionpack/test/activerecord (added)
+ * trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added)
+ * trunk/actionpack/test/activerecord/pagination_test.rb (added)
+ * trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted)
+ * trunk/actionpack/test/fixtures/companies.yml (added)
+ * trunk/actionpack/test/fixtures/company.rb (added)
+ * trunk/actionpack/test/fixtures/db_definitions (added)
+ * trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added)
+ * trunk/actionpack/test/fixtures/developer.rb (added)
+ * trunk/actionpack/test/fixtures/developers_projects.yml (added)
+ * trunk/actionpack/test/fixtures/developers.yml (added)
+ * trunk/actionpack/test/fixtures/project.rb (added)
+ * trunk/actionpack/test/fixtures/projects.yml (added)
+ * trunk/actionpack/test/fixtures/replies.yml (added)
+ * trunk/actionpack/test/fixtures/reply.rb (added)
+ * trunk/actionpack/test/fixtures/topic.rb (added)
+ * trunk/actionpack/test/fixtures/topics.yml (added)
+ * Fix pagination problems when using include
+ * Introduce Unit Tests for pagination
+ * Allow count to work with :include by using count distinct.
+
+ [Kevin Clark & Jeremy Hopple]
+
+11/05/05 02:10:29 bitsweat [2878]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Update paginator docs. Closes #2744.
+
+10/16/05 15:42:03 minam [2649]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Update/clean up AP documentation (rdoc)
+
+08/31/05 00:13:10 ulysses [2078]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Add option to specify the singular name used by pagination. Closes
+ #1960
+
+08/23/05 14:24:15 minam [2041]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ Add support for :include with pagination (subject to existing
+ constraints for :include with :limit and :offset) #1478
+ [michael@schubert.cx]
+
+07/15/05 20:27:38 david [1839]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ More pagination speed #1334 [Stefan Kaes]
+
+07/14/05 08:02:01 david [1832]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ * trunk/actionpack/test/controller/addresses_render_test.rb (modified)
+ Made pagination faster #1334 [Stefan Kaes]
+
+04/13/05 05:40:22 david [1159]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/activerecord/lib/active_record/base.rb (modified)
+ Fixed pagination to work with joins #1034 [scott@sigkill.org]
+
+04/02/05 09:11:17 david [1067]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/lib/action_controller/scaffolding.rb (modified)
+ * trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified)
+ * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified)
+ * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified)
+ Added pagination for scaffolding (10 items per page) #964
+ [mortonda@dgrmm.net]
+
+03/31/05 14:46:11 david [1048]
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ Improved the message display on the exception handler pages #963
+ [Johan Sorensen]
+
+03/27/05 00:04:07 david [1017]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ Fixed that pagination_helper would ignore :params #947 [Sebastian
+ Kanthak]
+
+03/22/05 13:09:44 david [976]
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ Fixed documentation and prepared for 0.11.0 release
+
+03/21/05 14:35:36 david [967]
+ * trunk/actionpack/lib/action_controller/pagination.rb (modified)
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
+ Tweaked the documentation
+
+03/20/05 23:12:05 david [949]
+ * trunk/actionpack/CHANGELOG (modified)
+ * trunk/actionpack/lib/action_controller.rb (modified)
+ * trunk/actionpack/lib/action_controller/pagination.rb (added)
+ * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added)
+ * trunk/activesupport/lib/active_support/core_ext/kernel.rb (added)
+ Added pagination support through both a controller and helper add-on
+ #817 [Sam Stephenson]
--- /dev/null
+Pagination
+==========
+
+To install:
+
+ script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
+
+This code was extracted from Rails trunk after the release 1.2.3.
+WARNING: this code is dead. It is unmaintained, untested and full of cruft.
+
+There is a much better pagination plugin called will_paginate.
+Install it like this and glance through the README:
+
+ script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
+
+It doesn't have the same API, but is in fact much nicer. You can
+have both plugins installed until you change your controller/view code that
+handles pagination. Then, simply uninstall classic_pagination.
--- /dev/null
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the classic_pagination plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the classic_pagination plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Pagination'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
--- /dev/null
+#--
+# Copyright (c) 2004-2006 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+require 'pagination'
+require 'pagination_helper'
+
+ActionController::Base.class_eval do
+ include ActionController::Pagination
+end
+
+ActionView::Base.class_eval do
+ include ActionView::Helpers::PaginationHelper
+end
--- /dev/null
+puts "\n\n" + File.read(File.dirname(__FILE__) + '/README')
--- /dev/null
+module ActionController
+ # === Action Pack pagination for Active Record collections
+ #
+ # The Pagination module aids in the process of paging large collections of
+ # Active Record objects. It offers macro-style automatic fetching of your
+ # model for multiple views, or explicit fetching for single actions. And if
+ # the magic isn't flexible enough for your needs, you can create your own
+ # paginators with a minimal amount of code.
+ #
+ # The Pagination module can handle as much or as little as you wish. In the
+ # controller, have it automatically query your model for pagination; or,
+ # if you prefer, create Paginator objects yourself.
+ #
+ # Pagination is included automatically for all controllers.
+ #
+ # For help rendering pagination links, see
+ # ActionView::Helpers::PaginationHelper.
+ #
+ # ==== Automatic pagination for every action in a controller
+ #
+ # class PersonController < ApplicationController
+ # model :person
+ #
+ # paginate :people, :order => 'last_name, first_name',
+ # :per_page => 20
+ #
+ # # ...
+ # end
+ #
+ # Each action in this controller now has access to a <tt>@people</tt>
+ # instance variable, which is an ordered collection of model objects for the
+ # current page (at most 20, sorted by last name and first name), and a
+ # <tt>@person_pages</tt> Paginator instance. The current page is determined
+ # by the <tt>params[:page]</tt> variable.
+ #
+ # ==== Pagination for a single action
+ #
+ # def list
+ # @person_pages, @people =
+ # paginate :people, :order => 'last_name, first_name'
+ # end
+ #
+ # Like the previous example, but explicitly creates <tt>@person_pages</tt>
+ # and <tt>@people</tt> for a single action, and uses the default of 10 items
+ # per page.
+ #
+ # ==== Custom/"classic" pagination
+ #
+ # def list
+ # @person_pages = Paginator.new self, Person.count, 10, params[:page]
+ # @people = Person.find :all, :order => 'last_name, first_name',
+ # :limit => @person_pages.items_per_page,
+ # :offset => @person_pages.current.offset
+ # end
+ #
+ # Explicitly creates the paginator from the previous example and uses
+ # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
+ #
+ module Pagination
+ unless const_defined?(:OPTIONS)
+ # A hash holding options for controllers using macro-style pagination
+ OPTIONS = Hash.new
+
+ # The default options for pagination
+ DEFAULT_OPTIONS = {
+ :class_name => nil,
+ :singular_name => nil,
+ :per_page => 10,
+ :conditions => nil,
+ :order_by => nil,
+ :order => nil,
+ :join => nil,
+ :joins => nil,
+ :count => nil,
+ :include => nil,
+ :select => nil,
+ :group => nil,
+ :parameter => 'page'
+ }
+ else
+ DEFAULT_OPTIONS[:group] = nil
+ end
+
+ def self.included(base) #:nodoc:
+ super
+ base.extend(ClassMethods)
+ end
+
+ def self.validate_options!(collection_id, options, in_action) #:nodoc:
+ options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
+
+ valid_options = DEFAULT_OPTIONS.keys
+ valid_options << :actions unless in_action
+
+ unknown_option_keys = options.keys - valid_options
+ raise ActionController::ActionControllerError,
+ "Unknown options: #{unknown_option_keys.join(', ')}" unless
+ unknown_option_keys.empty?
+
+ options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
+ options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
+ end
+
+ # Returns a paginator and a collection of Active Record model instances
+ # for the paginator's current page. This is designed to be used in a
+ # single action; to automatically paginate multiple actions, consider
+ # ClassMethods#paginate.
+ #
+ # +options+ are:
+ # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
+ # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
+ # camelizing the singular name
+ # <tt>:per_page</tt>:: the maximum number of items to include in a
+ # single page. Defaults to 10
+ # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
+ # Model.count
+ # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
+ # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
+ # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
+ # and Model.count
+ # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
+ # and Model.count
+ # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
+ # and Model.count
+ # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
+ #
+ # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
+ #
+ # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
+ #
+ def paginate(collection_id, options={})
+ Pagination.validate_options!(collection_id, options, true)
+ paginator_and_collection_for(collection_id, options)
+ end
+
+ # These methods become class methods on any controller
+ module ClassMethods
+ # Creates a +before_filter+ which automatically paginates an Active
+ # Record model for all actions in a controller (or certain actions if
+ # specified with the <tt>:actions</tt> option).
+ #
+ # +options+ are the same as PaginationHelper#paginate, with the addition
+ # of:
+ # <tt>:actions</tt>:: an array of actions for which the pagination is
+ # active. Defaults to +nil+ (i.e., every action)
+ def paginate(collection_id, options={})
+ Pagination.validate_options!(collection_id, options, false)
+ module_eval do
+ before_filter :create_paginators_and_retrieve_collections
+ OPTIONS[self] ||= Hash.new
+ OPTIONS[self][collection_id] = options
+ end
+ end
+ end
+
+ def create_paginators_and_retrieve_collections #:nodoc:
+ Pagination::OPTIONS[self.class].each do |collection_id, options|
+ next unless options[:actions].include? action_name if
+ options[:actions]
+
+ paginator, collection =
+ paginator_and_collection_for(collection_id, options)
+
+ paginator_name = "@#{options[:singular_name]}_pages"
+ self.instance_variable_set(paginator_name, paginator)
+
+ collection_name = "@#{collection_id.to_s}"
+ self.instance_variable_set(collection_name, collection)
+ end
+ end
+
+ # Returns the total number of items in the collection to be paginated for
+ # the +model+ and given +conditions+. Override this method to implement a
+ # custom counter.
+ def count_collection_for_pagination(model, options)
+ model.count(:conditions => options[:conditions],
+ :joins => options[:join] || options[:joins],
+ :include => options[:include],
+ :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
+ end
+
+ # Returns a collection of items for the given +model+ and +options[conditions]+,
+ # ordered by +options[order]+, for the current page in the given +paginator+.
+ # Override this method to implement a custom finder.
+ def find_collection_for_pagination(model, options, paginator)
+ model.find(:all, :conditions => options[:conditions],
+ :order => options[:order_by] || options[:order],
+ :joins => options[:join] || options[:joins], :include => options[:include],
+ :select => options[:select], :limit => options[:per_page],
+ :group => options[:group], :offset => paginator.current.offset)
+ end
+
+ protected :create_paginators_and_retrieve_collections,
+ :count_collection_for_pagination,
+ :find_collection_for_pagination
+
+ def paginator_and_collection_for(collection_id, options) #:nodoc:
+ klass = options[:class_name].constantize
+ page = params[options[:parameter]]
+ count = count_collection_for_pagination(klass, options)
+ paginator = Paginator.new(self, count, options[:per_page], page)
+ collection = find_collection_for_pagination(klass, options, paginator)
+
+ return paginator, collection
+ end
+
+ private :paginator_and_collection_for
+
+ # A class representing a paginator for an Active Record collection.
+ class Paginator
+ include Enumerable
+
+ # Creates a new Paginator on the given +controller+ for a set of items
+ # of size +item_count+ and having +items_per_page+ items per page.
+ # Raises ArgumentError if items_per_page is out of bounds (i.e., less
+ # than or equal to zero). The page CGI parameter for links defaults to
+ # "page" and can be overridden with +page_parameter+.
+ def initialize(controller, item_count, items_per_page, current_page=1)
+ raise ArgumentError, 'must have at least one item per page' if
+ items_per_page <= 0
+
+ @controller = controller
+ @item_count = item_count || 0
+ @items_per_page = items_per_page
+ @pages = {}
+
+ self.current_page = current_page
+ end
+ attr_reader :controller, :item_count, :items_per_page
+
+ # Sets the current page number of this paginator. If +page+ is a Page
+ # object, its +number+ attribute is used as the value; if the page does
+ # not belong to this Paginator, an ArgumentError is raised.
+ def current_page=(page)
+ if page.is_a? Page
+ raise ArgumentError, 'Page/Paginator mismatch' unless
+ page.paginator == self
+ end
+ page = page.to_i
+ @current_page_number = has_page_number?(page) ? page : 1
+ end
+
+ # Returns a Page object representing this paginator's current page.
+ def current_page
+ @current_page ||= self[@current_page_number]
+ end
+ alias current :current_page
+
+ # Returns a new Page representing the first page in this paginator.
+ def first_page
+ @first_page ||= self[1]
+ end
+ alias first :first_page
+
+ # Returns a new Page representing the last page in this paginator.
+ def last_page
+ @last_page ||= self[page_count]
+ end
+ alias last :last_page
+
+ # Returns the number of pages in this paginator.
+ def page_count
+ @page_count ||= @item_count.zero? ? 1 :
+ (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
+ end
+
+ alias length :page_count
+
+ # Returns true if this paginator contains the page of index +number+.
+ def has_page_number?(number)
+ number >= 1 and number <= page_count
+ end
+
+ # Returns a new Page representing the page with the given index
+ # +number+.
+ def [](number)
+ @pages[number] ||= Page.new(self, number)
+ end
+
+ # Successively yields all the paginator's pages to the given block.
+ def each(&block)
+ page_count.times do |n|
+ yield self[n+1]
+ end
+ end
+
+ # A class representing a single page in a paginator.
+ class Page
+ include Comparable
+
+ # Creates a new Page for the given +paginator+ with the index
+ # +number+. If +number+ is not in the range of valid page numbers or
+ # is not a number at all, it defaults to 1.
+ def initialize(paginator, number)
+ @paginator = paginator
+ @number = number.to_i
+ @number = 1 unless @paginator.has_page_number? @number
+ end
+ attr_reader :paginator, :number
+ alias to_i :number
+
+ # Compares two Page objects and returns true when they represent the
+ # same page (i.e., their paginators are the same and they have the
+ # same page number).
+ def ==(page)
+ return false if page.nil?
+ @paginator == page.paginator and
+ @number == page.number
+ end
+
+ # Compares two Page objects and returns -1 if the left-hand page comes
+ # before the right-hand page, 0 if the pages are equal, and 1 if the
+ # left-hand page comes after the right-hand page. Raises ArgumentError
+ # if the pages do not belong to the same Paginator object.
+ def <=>(page)
+ raise ArgumentError unless @paginator == page.paginator
+ @number <=> page.number
+ end
+
+ # Returns the item offset for the first item in this page.
+ def offset
+ @paginator.items_per_page * (@number - 1)
+ end
+
+ # Returns the number of the first item displayed.
+ def first_item
+ offset + 1
+ end
+
+ # Returns the number of the last item displayed.
+ def last_item
+ [@paginator.items_per_page * @number, @paginator.item_count].min
+ end
+
+ # Returns true if this page is the first page in the paginator.
+ def first?
+ self == @paginator.first
+ end
+
+ # Returns true if this page is the last page in the paginator.
+ def last?
+ self == @paginator.last
+ end
+
+ # Returns a new Page object representing the page just before this
+ # page, or nil if this is the first page.
+ def previous
+ if first? then nil else @paginator[@number - 1] end
+ end
+
+ # Returns a new Page object representing the page just after this
+ # page, or nil if this is the last page.
+ def next
+ if last? then nil else @paginator[@number + 1] end
+ end
+
+ # Returns a new Window object for this page with the specified
+ # +padding+.
+ def window(padding=2)
+ Window.new(self, padding)
+ end
+
+ # Returns the limit/offset array for this page.
+ def to_sql
+ [@paginator.items_per_page, offset]
+ end
+
+ def to_param #:nodoc:
+ @number.to_s
+ end
+ end
+
+ # A class for representing ranges around a given page.
+ class Window
+ # Creates a new Window object for the given +page+ with the specified
+ # +padding+.
+ def initialize(page, padding=2)
+ @paginator = page.paginator
+ @page = page
+ self.padding = padding
+ end
+ attr_reader :paginator, :page
+
+ # Sets the window's padding (the number of pages on either side of the
+ # window page).
+ def padding=(padding)
+ @padding = padding < 0 ? 0 : padding
+ # Find the beginning and end pages of the window
+ @first = @paginator.has_page_number?(@page.number - @padding) ?
+ @paginator[@page.number - @padding] : @paginator.first
+ @last = @paginator.has_page_number?(@page.number + @padding) ?
+ @paginator[@page.number + @padding] : @paginator.last
+ end
+ attr_reader :padding, :first, :last
+
+ # Returns an array of Page objects in the current window.
+ def pages
+ (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
+ end
+ alias to_a :pages
+ end
+ end
+
+ end
+end
--- /dev/null
+module ActionView
+ module Helpers
+ # Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
+ # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
+ #
+ # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
+ # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
+ module PaginationHelper
+ unless const_defined?(:DEFAULT_OPTIONS)
+ DEFAULT_OPTIONS = {
+ :name => :page,
+ :window_size => 2,
+ :always_show_anchors => true,
+ :link_to_current_page => false,
+ :params => {}
+ }
+ end
+
+ # Creates a basic HTML link bar for the given +paginator+. Links will be created
+ # for the next and/or previous page and for a number of other pages around the current
+ # pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
+ #
+ # ==== Options
+ # <tt>:name</tt>:: the routing name for this paginator
+ # (defaults to +page+)
+ # <tt>:prefix</tt>:: prefix for pagination links
+ # (i.e. Older Pages: 1 2 3 4)
+ # <tt>:suffix</tt>:: suffix for pagination links
+ # (i.e. 1 2 3 4 <- Older Pages)
+ # <tt>:window_size</tt>:: the number of pages to show around
+ # the current page (defaults to <tt>2</tt>)
+ # <tt>:always_show_anchors</tt>:: whether or not the first and last
+ # pages should always be shown
+ # (defaults to +true+)
+ # <tt>:link_to_current_page</tt>:: whether or not the current page
+ # should be linked to (defaults to
+ # +false+)
+ # <tt>:params</tt>:: any additional routing parameters
+ # for page URLs
+ #
+ # ==== Examples
+ # # We'll assume we have a paginator setup in @person_pages...
+ #
+ # pagination_links(@person_pages)
+ # # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
+ #
+ # pagination_links(@person_pages, :link_to_current_page => true)
+ # # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
+ #
+ # pagination_links(@person_pages, :always_show_anchors => false)
+ # # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
+ #
+ # pagination_links(@person_pages, :window_size => 1)
+ # # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
+ #
+ # pagination_links(@person_pages, :params => { :viewer => "flash" })
+ # # => 1 <a href="/?page=2&viewer=flash/">2</a> <a href="/?page=3&viewer=flash/">3</a> ...
+ # # <a href="/?page=10&viewer=flash/">10</a>
+ def pagination_links(paginator, options={}, html_options={})
+ name = options[:name] || DEFAULT_OPTIONS[:name]
+ params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
+
+ prefix = options[:prefix] || ''
+ suffix = options[:suffix] || ''
+
+ pagination_links_each(paginator, options, prefix, suffix) do |n|
+ params[name] = n
+ link_to(n.to_s, params, html_options)
+ end
+ end
+
+ # Iterate through the pages of a given +paginator+, invoking a
+ # block for each page number that needs to be rendered as a link.
+ #
+ # ==== Options
+ # <tt>:window_size</tt>:: the number of pages to show around
+ # the current page (defaults to +2+)
+ # <tt>:always_show_anchors</tt>:: whether or not the first and last
+ # pages should always be shown
+ # (defaults to +true+)
+ # <tt>:link_to_current_page</tt>:: whether or not the current page
+ # should be linked to (defaults to
+ # +false+)
+ #
+ # ==== Example
+ # # Turn paginated links into an Ajax call
+ # pagination_links_each(paginator, page_options) do |link|
+ # options = { :url => {:action => 'list'}, :update => 'results' }
+ # html_options = { :href => url_for(:action => 'list') }
+ #
+ # link_to_remote(link.to_s, options, html_options)
+ # end
+ def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
+ options = DEFAULT_OPTIONS.merge(options)
+ link_to_current_page = options[:link_to_current_page]
+ always_show_anchors = options[:always_show_anchors]
+
+ current_page = paginator.current_page
+ window_pages = current_page.window(options[:window_size]).pages
+ return if window_pages.length <= 1 unless link_to_current_page
+
+ first, last = paginator.first, paginator.last
+
+ html = ''
+
+ html << prefix if prefix
+
+ if always_show_anchors and not (wp_first = window_pages[0]).first?
+ html << yield(first.number)
+ html << ' ... ' if wp_first.number - first.number > 1
+ html << ' '
+ end
+
+ window_pages.each do |page|
+ if current_page == page && !link_to_current_page
+ html << page.number.to_s
+ else
+ html << yield(page.number)
+ end
+ html << ' '
+ end
+
+ if always_show_anchors and not (wp_last = window_pages[-1]).last?
+ html << ' ... ' if last.number - wp_last.number > 1
+ html << yield(last.number)
+ end
+
+ html << suffix if suffix
+
+ html
+ end
+
+ end # PaginationHelper
+ end # Helpers
+end # ActionView
--- /dev/null
+thirty_seven_signals:
+ id: 1
+ name: 37Signals
+ rating: 4
+
+TextDrive:
+ id: 2
+ name: TextDrive
+ rating: 4
+
+PlanetArgon:
+ id: 3
+ name: Planet Argon
+ rating: 4
+
+Google:
+ id: 4
+ name: Google
+ rating: 4
+
+Ionist:
+ id: 5
+ name: Ioni.st
+ rating: 4
\ No newline at end of file
--- /dev/null
+class Company < ActiveRecord::Base
+ attr_protected :rating
+ set_sequence_name :companies_nonstd_seq
+
+ validates_presence_of :name
+ def validate
+ errors.add('rating', 'rating should not be 2') if rating == 2
+ end
+end
\ No newline at end of file
--- /dev/null
+class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+end
+
+class DeVeLoPeR < ActiveRecord::Base
+ set_table_name "developers"
+end
--- /dev/null
+david:
+ id: 1
+ name: David
+ salary: 80000
+
+jamis:
+ id: 2
+ name: Jamis
+ salary: 150000
+
+<% for digit in 3..10 %>
+dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+ salary: 100000
+<% end %>
+
+poor_jamis:
+ id: 11
+ name: Jamis
+ salary: 9000
\ No newline at end of file
--- /dev/null
+david_action_controller:
+ developer_id: 1
+ project_id: 2
+ joined_on: 2004-10-10
+
+david_active_record:
+ developer_id: 1
+ project_id: 1
+ joined_on: 2004-10-10
+
+jamis_active_record:
+ developer_id: 2
+ project_id: 1
\ No newline at end of file
--- /dev/null
+class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers, :uniq => true
+end
--- /dev/null
+action_controller:
+ id: 2
+ name: Active Controller
+
+active_record:
+ id: 1
+ name: Active Record
--- /dev/null
+witty_retort:
+ id: 1
+ topic_id: 1
+ content: Birdman is better!
+ created_at: <%= 6.hours.ago.to_s(:db) %>
+ updated_at: nil
+
+another:
+ id: 2
+ topic_id: 2
+ content: Nuh uh!
+ created_at: <%= 1.hour.ago.to_s(:db) %>
+ updated_at: nil
\ No newline at end of file
--- /dev/null
+class Reply < ActiveRecord::Base
+ belongs_to :topic, :include => [:replies]
+
+ validates_presence_of :content
+end
--- /dev/null
+CREATE TABLE 'companies' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL,
+ 'rating' INTEGER DEFAULT 1
+);
+
+CREATE TABLE 'replies' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'content' text,
+ 'created_at' datetime,
+ 'updated_at' datetime,
+ 'topic_id' integer
+);
+
+CREATE TABLE 'topics' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'title' varchar(255),
+ 'subtitle' varchar(255),
+ 'content' text,
+ 'created_at' datetime,
+ 'updated_at' datetime
+);
+
+CREATE TABLE 'developers' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL,
+ 'salary' INTEGER DEFAULT 70000,
+ 'created_at' DATETIME DEFAULT NULL,
+ 'updated_at' DATETIME DEFAULT NULL
+);
+
+CREATE TABLE 'projects' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'developers_projects' (
+ 'developer_id' INTEGER NOT NULL,
+ 'project_id' INTEGER NOT NULL,
+ 'joined_on' DATE DEFAULT NULL,
+ 'access_level' INTEGER DEFAULT 1
+);
--- /dev/null
+class Topic < ActiveRecord::Base
+ has_many :replies, :include => [:user], :dependent => :destroy
+end
--- /dev/null
+futurama:
+ id: 1
+ title: Isnt futurama awesome?
+ subtitle: It really is, isnt it.
+ content: I like futurama
+ created_at: <%= 1.day.ago.to_s(:db) %>
+ updated_at:
+
+harvey_birdman:
+ id: 2
+ title: Harvey Birdman is the king of all men
+ subtitle: yup
+ content: It really is
+ created_at: <%= 2.hours.ago.to_s(:db) %>
+ updated_at:
+
+rails:
+ id: 3
+ title: Rails is nice
+ subtitle: It makes me happy
+ content: except when I have to hack internals to fix pagination. even then really.
+ created_at: <%= 20.minutes.ago.to_s(:db) %>
--- /dev/null
+require 'test/unit'
+
+unless defined?(ActiveRecord)
+ plugin_root = File.join(File.dirname(__FILE__), '..')
+
+ # first look for a symlink to a copy of the framework
+ if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
+ puts "found framework root: #{framework_root}"
+ # this allows for a plugin to be tested outside an app
+ $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
+ else
+ # is the plugin installed in an application?
+ app_root = plugin_root + '/../../..'
+
+ if File.directory? app_root + '/config'
+ puts 'using config/boot.rb'
+ ENV['RAILS_ENV'] = 'test'
+ require File.expand_path(app_root + '/config/boot')
+ else
+ # simply use installed gems if available
+ puts 'using rubygems'
+ require 'rubygems'
+ gem 'actionpack'; gem 'activerecord'
+ end
+ end
+
+ %w(action_pack active_record action_controller active_record/fixtures action_controller/test_process).each {|f| require f}
+
+ Dependencies.load_paths.unshift "#{plugin_root}/lib"
+end
+
+# Define the connector
+class ActiveRecordTestConnector
+ cattr_accessor :able_to_connect
+ cattr_accessor :connected
+
+ # Set our defaults
+ self.connected = false
+ self.able_to_connect = true
+
+ class << self
+ def setup
+ unless self.connected || !self.able_to_connect
+ setup_connection
+ load_schema
+ require_fixture_models
+ self.connected = true
+ end
+ rescue Exception => e # errors from ActiveRecord setup
+ $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
+ #$stderr.puts " #{e.backtrace.join("\n ")}\n"
+ self.able_to_connect = false
+ end
+
+ private
+
+ def setup_connection
+ if Object.const_defined?(:ActiveRecord)
+ defaults = { :database => ':memory:' }
+ begin
+ options = defaults.merge :adapter => 'sqlite3', :timeout => 500
+ ActiveRecord::Base.establish_connection(options)
+ ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
+ ActiveRecord::Base.connection
+ rescue Exception # errors from establishing a connection
+ $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.'
+ options = defaults.merge :adapter => 'sqlite'
+ ActiveRecord::Base.establish_connection(options)
+ ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options }
+ ActiveRecord::Base.connection
+ end
+
+ Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
+ else
+ raise "Can't setup connection since ActiveRecord isn't loaded."
+ end
+ end
+
+ # Load actionpack sqlite tables
+ def load_schema
+ File.read(File.dirname(__FILE__) + "/fixtures/schema.sql").split(';').each do |sql|
+ ActiveRecord::Base.connection.execute(sql) unless sql.blank?
+ end
+ end
+
+ def require_fixture_models
+ Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
+ end
+ end
+end
+
+# Test case for inheritance
+class ActiveRecordTestCase < Test::Unit::TestCase
+ # Set our fixture path
+ if ActiveRecordTestConnector.able_to_connect
+ self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/"
+ self.use_transactional_fixtures = false
+ end
+
+ def self.fixtures(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+
+ def run(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+
+ # Default so Test::Unit::TestCase doesn't complain
+ def test_truth
+ end
+end
+
+ActiveRecordTestConnector.setup
+ActionController::Routing::Routes.reload rescue nil
+ActionController::Routing::Routes.draw do |map|
+ map.connect ':controller/:action/:id'
+end
--- /dev/null
+require File.dirname(__FILE__) + '/helper'
+require File.dirname(__FILE__) + '/../init'
+
+class PaginationHelperTest < Test::Unit::TestCase
+ include ActionController::Pagination
+ include ActionView::Helpers::PaginationHelper
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::TagHelper
+
+ def setup
+ @controller = Class.new do
+ attr_accessor :url, :request
+ def url_for(options, *parameters_for_method_reference)
+ url
+ end
+ end
+ @controller = @controller.new
+ @controller.url = "http://www.example.com"
+ end
+
+ def test_pagination_links
+ total, per_page, page = 30, 10, 1
+ output = pagination_links Paginator.new(@controller, total, per_page, page)
+ assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
+ end
+
+ def test_pagination_links_with_prefix
+ total, per_page, page = 30, 10, 1
+ output = pagination_links Paginator.new(@controller, total, per_page, page), :prefix => 'Newer '
+ assert_equal "Newer 1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
+ end
+
+ def test_pagination_links_with_suffix
+ total, per_page, page = 30, 10, 1
+ output = pagination_links Paginator.new(@controller, total, per_page, page), :suffix => 'Older'
+ assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> Older", output
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/helper'
+require File.dirname(__FILE__) + '/../init'
+
+class PaginationTest < ActiveRecordTestCase
+ fixtures :topics, :replies, :developers, :projects, :developers_projects
+
+ class PaginationController < ActionController::Base
+ if respond_to? :view_paths=
+ self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ]
+ else
+ self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ]
+ end
+
+ def simple_paginate
+ @topic_pages, @topics = paginate(:topics)
+ render :nothing => true
+ end
+
+ def paginate_with_per_page
+ @topic_pages, @topics = paginate(:topics, :per_page => 1)
+ render :nothing => true
+ end
+
+ def paginate_with_order
+ @topic_pages, @topics = paginate(:topics, :order => 'created_at asc')
+ render :nothing => true
+ end
+
+ def paginate_with_order_by
+ @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc')
+ render :nothing => true
+ end
+
+ def paginate_with_include_and_order
+ @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc')
+ render :nothing => true
+ end
+
+ def paginate_with_conditions
+ @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago])
+ render :nothing => true
+ end
+
+ def paginate_with_class_name
+ @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR")
+ render :nothing => true
+ end
+
+ def paginate_with_singular_name
+ @developer_pages, @developers = paginate()
+ render :nothing => true
+ end
+
+ def paginate_with_joins
+ @developer_pages, @developers = paginate(:developers,
+ :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
+ :conditions => 'project_id=1')
+ render :nothing => true
+ end
+
+ def paginate_with_join
+ @developer_pages, @developers = paginate(:developers,
+ :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
+ :conditions => 'project_id=1')
+ render :nothing => true
+ end
+
+ def paginate_with_join_and_count
+ @developer_pages, @developers = paginate(:developers,
+ :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id',
+ :conditions => 'project_id=1',
+ :count => "d.id")
+ render :nothing => true
+ end
+
+ def paginate_with_join_and_group
+ @developer_pages, @developers = paginate(:developers,
+ :join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id',
+ :group => 'developers.id')
+ render :nothing => true
+ end
+
+ def rescue_errors(e) raise e end
+
+ def rescue_action(e) raise end
+
+ end
+
+ def setup
+ @controller = PaginationController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ super
+ end
+
+ # Single Action Pagination Tests
+
+ def test_simple_paginate
+ get :simple_paginate
+ assert_equal 1, assigns(:topic_pages).page_count
+ assert_equal 3, assigns(:topics).size
+ end
+
+ def test_paginate_with_per_page
+ get :paginate_with_per_page
+ assert_equal 1, assigns(:topics).size
+ assert_equal 3, assigns(:topic_pages).page_count
+ end
+
+ def test_paginate_with_order
+ get :paginate_with_order
+ expected = [topics(:futurama),
+ topics(:harvey_birdman),
+ topics(:rails)]
+ assert_equal expected, assigns(:topics)
+ assert_equal 1, assigns(:topic_pages).page_count
+ end
+
+ def test_paginate_with_order_by
+ get :paginate_with_order
+ expected = assigns(:topics)
+ get :paginate_with_order_by
+ assert_equal expected, assigns(:topics)
+ assert_equal 1, assigns(:topic_pages).page_count
+ end
+
+ def test_paginate_with_conditions
+ get :paginate_with_conditions
+ expected = [topics(:rails)]
+ assert_equal expected, assigns(:topics)
+ assert_equal 1, assigns(:topic_pages).page_count
+ end
+
+ def test_paginate_with_class_name
+ get :paginate_with_class_name
+
+ assert assigns(:developers).size > 0
+ assert_equal DeVeLoPeR, assigns(:developers).first.class
+ end
+
+ def test_paginate_with_joins
+ get :paginate_with_joins
+ assert_equal 2, assigns(:developers).size
+ developer_names = assigns(:developers).map { |d| d.name }
+ assert developer_names.include?('David')
+ assert developer_names.include?('Jamis')
+ end
+
+ def test_paginate_with_join_and_conditions
+ get :paginate_with_joins
+ expected = assigns(:developers)
+ get :paginate_with_join
+ assert_equal expected, assigns(:developers)
+ end
+
+ def test_paginate_with_join_and_count
+ get :paginate_with_joins
+ expected = assigns(:developers)
+ get :paginate_with_join_and_count
+ assert_equal expected, assigns(:developers)
+ end
+
+ def test_paginate_with_include_and_order
+ get :paginate_with_include_and_order
+ expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10)
+ assert_equal expected, assigns(:topics)
+ end
+
+ def test_paginate_with_join_and_group
+ get :paginate_with_join_and_group
+ assert_equal 2, assigns(:developers).size
+ assert_equal 2, assigns(:developer_pages).item_count
+ developer_names = assigns(:developers).map { |d| d.name }
+ assert developer_names.include?('David')
+ assert developer_names.include?('Jamis')
+ end
+end
--- /dev/null
+= CodeRay - Trunk folder structure\r
+\r
+== bench - Benchmarking system\r
+\r
+All benchmarking stuff goes here.\r
+\r
+Test inputs are stored in files named <code>example.<lang></code>.\r
+Test outputs go to <code>bench/test.<encoder-default-file-extension></code>.\r
+\r
+Run <code>bench/bench.rb</code> to get a usage description.\r
+\r
+Run <code>rake bench</code> to perform an example benchmark.\r
+\r
+\r
+== bin - Scripts\r
+\r
+Executional files for CodeRay.\r
+\r
+\r
+== demo - Demos and functional tests\r
+\r
+Demonstrational scripts to show of CodeRay's features.\r
+\r
+Run them as functional tests with <code>rake test:demos</code>.\r
+\r
+\r
+== etc - Lots of stuff\r
+\r
+Some addidtional files for CodeRay, mainly graphics and Vim scripts.\r
+\r
+\r
+== gem_server - Gem output folder\r
+\r
+For <code>rake gem</code>.\r
+\r
+\r
+== lib - CodeRay library code\r
+\r
+This is the base directory for the CodeRay library.\r
+\r
+\r
+== rake_helpers - Rake helper libraries\r
+\r
+Some files to enhance Rake, including the Autumnal Rdoc template and some scripts.\r
+\r
+\r
+== test - Tests\r
+\r
+Tests for the scanners.\r
+\r
+Each language has its own subfolder and sub-suite.\r
+\r
+Run with <code>rake test</code>.\r
--- /dev/null
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+\f
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+\f
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
--- /dev/null
+= CodeRay
+
+[- Tired of blue'n'gray? Try the original version of this documentation on
+http://rd.cYcnus.de/coderay/doc (use Ctrl+Click to open it in its own frame.) -]
+
+== About
+CodeRay is a Ruby library for syntax highlighting.
+
+Syntax highlighting means: You put your code in, and you get it back colored;
+Keywords, strings, floats, comments - all in different colors.
+And with line numbers.
+
+*Syntax* *Highlighting*...
+* makes code easier to read and maintain
+* lets you detect syntax errors faster
+* helps you to understand the syntax of a language
+* looks nice
+* is what everybody should have on their website
+* solves all your problems and makes the girls run after you
+
+Version: 0.7.4 (2006.october.20)
+Author:: murphy (Kornelius Kalnbach)
+Contact:: murphy rubychan de
+Website:: coderay.rubychan.de[http://coderay.rubychan.de]
+License:: GNU LGPL; see LICENSE file in the main directory.
+Subversion:: $Id: README 219 2006-10-20 15:52:25Z murphy $
+
+-----
+
+== Installation
+
+You need RubyGems[http://rubyforge.org/frs/?group_id=126].
+
+ % gem install coderay
+
+Since CodeRay is still in beta stage, nightly buildy may be useful:
+
+ % gem install coderay -rs rd.cYcnus.de/coderay
+
+
+=== Dependencies
+
+CodeRay needs Ruby 1.8 and the
+strscan[http://www.ruby-doc.org/stdlib/libdoc/strscan/rdoc/index.htm]
+library (part of the standard library.) It should also run with Ruby 1.9 and
+yarv.
+
+
+== Example Usage
+(Forgive me, but this is not highlighted.)
+
+ require 'coderay'
+
+ tokens = CodeRay.scan "puts 'Hello, world!'", :ruby
+ page = tokens.html :line_numbers => :inline, :wrap => :page
+ puts page
+
+
+== Documentation
+
+See CodeRay.
+
+Please report errors in this documentation to <coderay cycnus de>.
+
+
+-----
+
+== Credits
+
+=== Special Thanks to
+
+* licenser (Heinz N. Gies) for ending my QBasic career, inventing the Coder
+ project and the input/output plugin system.
+ CodeRay would not exist without him.
+
+=== Thanks to
+
+* Caleb Clausen for writing RubyLexer (see
+ http://rubyforge.org/projects/rubylexer) and lots of very interesting mail
+ traffic
+* birkenfeld (Georg Brandl) and mitsuhiku (Arnim Ronacher) for PyKleur. You
+ guys rock!
+* Jamis Buck for writing Syntax (see http://rubyforge.org/projects/syntax)
+ I got some useful ideas from it.
+* Doug Kearns and everyone else who worked on ruby.vim - it not only helped me
+ coding CodeRay, but also gave me a wonderful target to reach for the Ruby
+ scanner.
+* everyone who used CodeBB on http://www.rubyforen.de and
+ http://www.infhu.de/mx
+* iGEL, magichisoka, manveru, WoNáDo and everyone I forgot from rubyforen.de
+* Daniel and Dethix from ruby-mine.de
+* Dookie (who is no longer with us...) and Leonidas from
+ http://www.python-forum.de
+* Andreas Schwarz for finding out that CaseIgnoringWordList was not case
+ ignoring! Such things really make you write tests.
+* matz and all Ruby gods and gurus
+* The inventors of: the computer, the internet, the true color display, HTML &
+ CSS, VIM, RUBY, pizza, microwaves, guitars, scouting, programming, anime,
+ manga, coke and green ice tea.
+
+Where would we be without all those people?
+
+=== Created using
+
+* Ruby[http://ruby-lang.org/]
+* Chihiro (my Sony VAIO laptop), Henrietta (my new MacBook) and
+ Seras (my Athlon 2200+ tower)
+* VIM[http://vim.org] and TextMate[http://macromates.com]
+* RDE[http://homepage2.nifty.com/sakazuki/rde_e.html]
+* Microsoft Windows (yes, I confess!) and MacOS X
+* Firefox[http://www.mozilla.org/products/firefox/] and
+ Thunderbird[http://www.mozilla.org/products/thunderbird/]
+* Rake[http://rake.rubyforge.org/]
+* RubyGems[http://docs.rubygems.org/]
+* {Subversion/TortoiseSVN}[http://tortoisesvn.tigris.org/] using Apache via
+ XAMPP[http://www.apachefriends.org/en/xampp.html]
+* RDoc (though I'm quite unsatisfied with it)
+* GNUWin32, MinGW and some other tools to make the shell under windows a bit
+ more useful
+* Term::ANSIColor[http://term-ansicolor.rubyforge.org/]
+
+---
+
+* As you can see, CodeRay was created under heavy use of *free* software.
+* So CodeRay is also *free*.
+* If you use CodeRay to create software, think about making this software
+ *free*, too.
+* Thanks :)
--- /dev/null
+#!/usr/bin/env ruby\r
+# CodeRay Executable\r
+#\r
+# Version: 0.1\r
+# Author: murphy\r
+\r
+def err msg\r
+ $stderr.puts msg\r
+end\r
+\r
+begin\r
+ require 'coderay'\r
+\r
+ if ARGV.empty?\r
+ puts <<-USAGE\r
+CodeRay #{CodeRay::VERSION} (http://rd.cYcnus.de/coderay)\r
+Usage:\r
+ coderay -<lang> [-<format>] < file > output\r
+ coderay file [-<format>]\r
+Example:\r
+ coderay -ruby -statistic < foo.rb\r
+ coderay codegen.c # generates codegen.c.html\r
+ USAGE\r
+ end\r
+\r
+ first, second = ARGV\r
+\r
+ if first\r
+ if first[/-(\w+)/] == first\r
+ lang = $1.to_sym\r
+ input = $stdin.read\r
+ tokens = :scan\r
+ elsif first == '-'\r
+ lang = $1.to_sym\r
+ input = $stdin.read\r
+ tokens = :scan\r
+ else\r
+ file = first\r
+ tokens = CodeRay.scan_file file\r
+ output_filename, output_ext = file, /#{Regexp.escape(File.extname(file))}$/\r
+ end\r
+ else\r
+ puts 'No lang/file given.'\r
+ exit 1\r
+ end\r
+\r
+ if second\r
+ if second[/-(\w+)/] == second\r
+ format = $1.to_sym\r
+ else\r
+ raise 'Invalid format (must be -xxx).'\r
+ end\r
+ else\r
+ $stderr.puts 'No format given; setting to default (HTML Page)'\r
+ format = :page\r
+ end\r
+ \r
+ # TODO: allow streaming\r
+ if tokens == :scan\r
+ output = CodeRay::Duo[lang => format].highlight input #, :stream => true\r
+ else\r
+ output = tokens.encode format\r
+ end\r
+ out = $stdout\r
+ if output_filename\r
+ output_filename += '.' + CodeRay::Encoders[format]::FILE_EXTENSION\r
+ if File.exist? output_filename\r
+ err 'File %s already exists.' % output_filename\r
+ exit\r
+ else\r
+ out = File.open output_filename, 'w'\r
+ end\r
+ end\r
+ out.print output\r
+\r
+rescue => boom\r
+ err "Error: #{boom.message}\n"\r
+ err boom.backtrace\r
+ err '-' * 50\r
+ err ARGV\r
+ exit 1\r
+end\r
--- /dev/null
+#!/usr/bin/env ruby\r
+require 'coderay'\r
+\r
+puts CodeRay::Encoders[:html]::CSS.new.stylesheet\r
--- /dev/null
+# = CodeRay Library
+#
+# $Id: coderay.rb 227 2007-04-24 12:26:18Z murphy $
+#
+# CodeRay is a Ruby library for syntax highlighting.
+#
+# I try to make CodeRay easy to use and intuitive, but at the same time fully featured, complete,
+# fast and efficient.
+#
+# See README.
+#
+# It consists mainly of
+# * the main engine: CodeRay (Scanners::Scanner, Tokens/TokenStream, Encoders::Encoder), PluginHost
+# * the scanners in CodeRay::Scanners
+# * the encoders in CodeRay::Encoders
+#
+# Here's a fancy graphic to light up this gray docu:
+#
+# http://rd.cYcnus.de/coderay/scheme.png
+#
+# == Documentation
+#
+# See CodeRay, Encoders, Scanners, Tokens.
+#
+# == Usage
+#
+# Remember you need RubyGems to use CodeRay, unless you have it in your load path. Run Ruby with
+# -rubygems option if required.
+#
+# === Highlight Ruby code in a string as html
+#
+# require 'coderay'
+# print CodeRay.scan('puts "Hello, world!"', :ruby).html
+#
+# # prints something like this:
+# puts <span class="s">"Hello, world!"</span>
+#
+#
+# === Highlight C code from a file in a html div
+#
+# require 'coderay'
+# print CodeRay.scan(File.read('ruby.h'), :c).div
+# print CodeRay.scan_file('ruby.h').html.div
+#
+# You can include this div in your page. The used CSS styles can be printed with
+#
+# % coderay_stylesheet
+#
+# === Highlight without typing too much
+#
+# If you are one of the hasty (or lazy, or extremely curious) people, just run this file:
+#
+# % ruby -rubygems /path/to/coderay/coderay.rb > example.html
+#
+# and look at the file it created in your browser.
+#
+# = CodeRay Module
+#
+# The CodeRay module provides convenience methods for the engine.
+#
+# * The +lang+ and +format+ arguments select Scanner and Encoder to use. These are
+# simply lower-case symbols, like <tt>:python</tt> or <tt>:html</tt>.
+# * All methods take an optional hash as last parameter, +options+, that is send to
+# the Encoder / Scanner.
+# * Input and language are always sorted in this order: +code+, +lang+.
+# (This is in alphabetical order, if you need a mnemonic ;)
+#
+# You should be able to highlight everything you want just using these methods;
+# so there is no need to dive into CodeRay's deep class hierarchy.
+#
+# The examples in the demo directory demonstrate common cases using this interface.
+#
+# = Basic Access Ways
+#
+# Read this to get a general view what CodeRay provides.
+#
+# == Scanning
+#
+# Scanning means analysing an input string, splitting it up into Tokens.
+# Each Token knows about what type it is: string, comment, class name, etc.
+#
+# Each +lang+ (language) has its own Scanner; for example, <tt>:ruby</tt> code is
+# handled by CodeRay::Scanners::Ruby.
+#
+# CodeRay.scan:: Scan a string in a given language into Tokens.
+# This is the most common method to use.
+# CodeRay.scan_file:: Scan a file and guess the language using FileType.
+#
+# The Tokens object you get from these methods can encode itself; see Tokens.
+#
+# == Encoding
+#
+# Encoding means compiling Tokens into an output. This can be colored HTML or
+# LaTeX, a textual statistic or just the number of non-whitespace tokens.
+#
+# Each Encoder provides output in a specific +format+, so you select Encoders via
+# formats like <tt>:html</tt> or <tt>:statistic</tt>.
+#
+# CodeRay.encode:: Scan and encode a string in a given language.
+# CodeRay.encode_tokens:: Encode the given tokens.
+# CodeRay.encode_file:: Scan a file, guess the language using FileType and encode it.
+#
+# == Streaming
+#
+# Streaming saves RAM by running Scanner and Encoder in some sort of
+# pipe mode; see TokenStream.
+#
+# CodeRay.scan_stream:: Scan in stream mode.
+#
+# == All-in-One Encoding
+#
+# CodeRay.encode:: Highlight a string with a given input and output format.
+#
+# == Instanciating
+#
+# You can use an Encoder instance to highlight multiple inputs. This way, the setup
+# for this Encoder must only be done once.
+#
+# CodeRay.encoder:: Create an Encoder instance with format and options.
+# CodeRay.scanner:: Create an Scanner instance for lang, with '' as default code.
+#
+# To make use of CodeRay.scanner, use CodeRay::Scanner::code=.
+#
+# The scanning methods provide more flexibility; we recommend to use these.
+#
+# == Reusing Scanners and Encoders
+#
+# If you want to re-use scanners and encoders (because that is faster), see
+# CodeRay::Duo for the most convenient (and recommended) interface.
+module CodeRay
+
+ # Version: Major.Minor.Teeny[.Revision]
+ # Major: 0 for pre-release
+ # Minor: odd for beta, even for stable
+ # Teeny: development state
+ # Revision: Subversion Revision number (generated on rake)
+ VERSION = '0.7.6'
+
+ require 'coderay/tokens'
+ require 'coderay/scanner'
+ require 'coderay/encoder'
+ require 'coderay/duo'
+ require 'coderay/style'
+
+
+ class << self
+
+ # Scans the given +code+ (a String) with the Scanner for +lang+.
+ #
+ # This is a simple way to use CodeRay. Example:
+ # require 'coderay'
+ # page = CodeRay.scan("puts 'Hello, world!'", :ruby).html
+ #
+ # See also demo/demo_simple.
+ def scan code, lang, options = {}, &block
+ scanner = Scanners[lang].new code, options, &block
+ scanner.tokenize
+ end
+
+ # Scans +filename+ (a path to a code file) with the Scanner for +lang+.
+ #
+ # If +lang+ is :auto or omitted, the CodeRay::FileType module is used to
+ # determine it. If it cannot find out what type it is, it uses
+ # CodeRay::Scanners::Plaintext.
+ #
+ # Calls CodeRay.scan.
+ #
+ # Example:
+ # require 'coderay'
+ # page = CodeRay.scan_file('some_c_code.c').html
+ def scan_file filename, lang = :auto, options = {}, &block
+ file = IO.read filename
+ if lang == :auto
+ require 'coderay/helpers/file_type'
+ lang = FileType.fetch filename, :plaintext, true
+ end
+ scan file, lang, options = {}, &block
+ end
+
+ # Scan the +code+ (a string) with the scanner for +lang+.
+ #
+ # Calls scan.
+ #
+ # See CodeRay.scan.
+ def scan_stream code, lang, options = {}, &block
+ options[:stream] = true
+ scan code, lang, options, &block
+ end
+
+ # Encode a string in Streaming mode.
+ #
+ # This starts scanning +code+ with the the Scanner for +lang+
+ # while encodes the output with the Encoder for +format+.
+ # +options+ will be passed to the Encoder.
+ #
+ # See CodeRay::Encoder.encode_stream
+ def encode_stream code, lang, format, options = {}
+ encoder(format, options).encode_stream code, lang, options
+ end
+
+ # Encode a string.
+ #
+ # This scans +code+ with the the Scanner for +lang+ and then
+ # encodes it with the Encoder for +format+.
+ # +options+ will be passed to the Encoder.
+ #
+ # See CodeRay::Encoder.encode
+ def encode code, lang, format, options = {}
+ encoder(format, options).encode code, lang, options
+ end
+
+ # Highlight a string into a HTML <div>.
+ #
+ # CSS styles use classes, so you have to include a stylesheet
+ # in your output.
+ #
+ # See encode.
+ def highlight code, lang, options = { :css => :class }, format = :div
+ encode code, lang, format, options
+ end
+
+ # Encode pre-scanned Tokens.
+ # Use this together with CodeRay.scan:
+ #
+ # require 'coderay'
+ #
+ # # Highlight a short Ruby code example in a HTML span
+ # tokens = CodeRay.scan '1 + 2', :ruby
+ # puts CodeRay.encode_tokens(tokens, :span)
+ #
+ def encode_tokens tokens, format, options = {}
+ encoder(format, options).encode_tokens tokens, options
+ end
+
+ # Encodes +filename+ (a path to a code file) with the Scanner for +lang+.
+ #
+ # See CodeRay.scan_file.
+ # Notice that the second argument is the output +format+, not the input language.
+ #
+ # Example:
+ # require 'coderay'
+ # page = CodeRay.encode_file 'some_c_code.c', :html
+ def encode_file filename, format, options = {}
+ tokens = scan_file filename, :auto, get_scanner_options(options)
+ encode_tokens tokens, format, options
+ end
+
+ # Highlight a file into a HTML <div>.
+ #
+ # CSS styles use classes, so you have to include a stylesheet
+ # in your output.
+ #
+ # See encode.
+ def highlight_file filename, options = { :css => :class }, format = :div
+ encode_file filename, format, options
+ end
+
+ # Finds the Encoder class for +format+ and creates an instance, passing
+ # +options+ to it.
+ #
+ # Example:
+ # require 'coderay'
+ #
+ # stats = CodeRay.encoder(:statistic)
+ # stats.encode("puts 17 + 4\n", :ruby)
+ #
+ # puts '%d out of %d tokens have the kind :integer.' % [
+ # stats.type_stats[:integer].count,
+ # stats.real_token_count
+ # ]
+ # #-> 2 out of 4 tokens have the kind :integer.
+ def encoder format, options = {}
+ Encoders[format].new options
+ end
+
+ # Finds the Scanner class for +lang+ and creates an instance, passing
+ # +options+ to it.
+ #
+ # See Scanner.new.
+ def scanner lang, options = {}
+ Scanners[lang].new '', options
+ end
+
+ # Extract the options for the scanner from the +options+ hash.
+ #
+ # Returns an empty Hash if <tt>:scanner_options</tt> is not set.
+ #
+ # This is used if a method like CodeRay.encode has to provide options
+ # for Encoder _and_ scanner.
+ def get_scanner_options options
+ options.fetch :scanner_options, {}
+ end
+
+ end
+
+ # This Exception is raised when you try to stream with something that is not
+ # capable of streaming.
+ class NotStreamableError < Exception
+ def initialize obj
+ @obj = obj
+ end
+
+ def to_s
+ '%s is not Streamable!' % @obj.class
+ end
+ end
+
+ # A dummy module that is included by subclasses of CodeRay::Scanner an CodeRay::Encoder
+ # to show that they are able to handle streams.
+ module Streamable
+ end
+
+end
+
+# Run a test script.
+if $0 == __FILE__
+ $stderr.print 'Press key to print demo.'; gets
+ code = File.read(__FILE__)[/module CodeRay.*/m]
+ print CodeRay.scan(code, :ruby).html
+end
--- /dev/null
+module CodeRay
+
+ # = Duo
+ #
+ # $Id: scanner.rb 123 2006-03-21 14:46:34Z murphy $
+ #
+ # A Duo is a convenient way to use CodeRay. You just create a Duo,
+ # giving it a lang (language of the input code) and a format (desired
+ # output format), and call Duo#highlight with the code.
+ #
+ # Duo makes it easy to re-use both scanner and encoder for a repetitive
+ # task. It also provides a very easy interface syntax:
+ #
+ # require 'coderay'
+ # CodeRay::Duo[:python, :div].highlight 'import this'
+ #
+ # Until you want to do uncommon things with CodeRay, I recommend to use
+ # this method, since it takes care of everything.
+ class Duo
+
+ attr_accessor :lang, :format, :options
+
+ # Create a new Duo, holding a lang and a format to highlight code.
+ #
+ # simple:
+ # CodeRay::Duo[:ruby, :page].highlight 'bla 42'
+ #
+ # streaming:
+ # CodeRay::Duo[:ruby, :page].highlight 'bar 23', :stream => true
+ #
+ # with options:
+ # CodeRay::Duo[:ruby, :html, :hint => :debug].highlight '????::??'
+ #
+ # alternative syntax without options:
+ # CodeRay::Duo[:ruby => :statistic].encode 'class << self; end'
+ #
+ # alternative syntax with options:
+ # CodeRay::Duo[{ :ruby => :statistic }, :do => :something].encode 'abc'
+ #
+ # The options are forwarded to scanner and encoder
+ # (see CodeRay.get_scanner_options).
+ def initialize lang = nil, format = nil, options = {}
+ if format == nil and lang.is_a? Hash and lang.size == 1
+ @lang = lang.keys.first
+ @format = lang[@lang]
+ else
+ @lang = lang
+ @format = format
+ end
+ @options = options
+ end
+
+ class << self
+ # To allow calls like Duo[:ruby, :html].highlight.
+ alias [] new
+ end
+
+ # The scanner of the duo. Only created once.
+ def scanner
+ @scanner ||= CodeRay.scanner @lang, CodeRay.get_scanner_options(@options)
+ end
+
+ # The encoder of the duo. Only created once.
+ def encoder
+ @encoder ||= CodeRay.encoder @format, @options
+ end
+
+ # Tokenize and highlight the code using +scanner+ and +encoder+.
+ #
+ # If the :stream option is set, the Duo will go into streaming mode,
+ # saving memory for the cost of time.
+ def encode code, options = { :stream => false }
+ stream = options.delete :stream
+ options = @options.merge options
+ if stream
+ encoder.encode_stream(code, @lang, options)
+ else
+ scanner.code = code
+ encoder.encode_tokens(scanner.tokenize, options)
+ end
+ end
+ alias highlight encode
+
+ end
+
+end
+
--- /dev/null
+require "stringio"
+
+module CodeRay
+
+ # This module holds the Encoder class and its subclasses.
+ # For example, the HTML encoder is named CodeRay::Encoders::HTML
+ # can be found in coderay/encoders/html.
+ #
+ # Encoders also provides methods and constants for the register
+ # mechanism and the [] method that returns the Encoder class
+ # belonging to the given format.
+ module Encoders
+ extend PluginHost
+ plugin_path File.dirname(__FILE__), 'encoders'
+
+ # = Encoder
+ #
+ # The Encoder base class. Together with Scanner and
+ # Tokens, it forms the highlighting triad.
+ #
+ # Encoder instances take a Tokens object and do something with it.
+ #
+ # The most common Encoder is surely the HTML encoder
+ # (CodeRay::Encoders::HTML). It highlights the code in a colorful
+ # html page.
+ # If you want the highlighted code in a div or a span instead,
+ # use its subclasses Div and Span.
+ class Encoder
+ extend Plugin
+ plugin_host Encoders
+
+ attr_reader :token_stream
+
+ class << self
+
+ # Returns if the Encoder can be used in streaming mode.
+ def streamable?
+ is_a? Streamable
+ end
+
+ # If FILE_EXTENSION isn't defined, this method returns the
+ # downcase class name instead.
+ def const_missing sym
+ if sym == :FILE_EXTENSION
+ plugin_id
+ else
+ super
+ end
+ end
+
+ end
+
+ # Subclasses are to store their default options in this constant.
+ DEFAULT_OPTIONS = { :stream => false }
+
+ # The options you gave the Encoder at creating.
+ attr_accessor :options
+
+ # Creates a new Encoder.
+ # +options+ is saved and used for all encode operations, as long
+ # as you don't overwrite it there by passing additional options.
+ #
+ # Encoder objects provide three encode methods:
+ # - encode simply takes a +code+ string and a +lang+
+ # - encode_tokens expects a +tokens+ object instead
+ # - encode_stream is like encode, but uses streaming mode.
+ #
+ # Each method has an optional +options+ parameter. These are
+ # added to the options you passed at creation.
+ def initialize options = {}
+ @options = self.class::DEFAULT_OPTIONS.merge options
+ raise "I am only the basic Encoder class. I can't encode "\
+ "anything. :( Use my subclasses." if self.class == Encoder
+ end
+
+ # Encode a Tokens object.
+ def encode_tokens tokens, options = {}
+ options = @options.merge options
+ setup options
+ compile tokens, options
+ finish options
+ end
+
+ # Encode the given +code+ after tokenizing it using the Scanner
+ # for +lang+.
+ def encode code, lang, options = {}
+ options = @options.merge options
+ scanner_options = CodeRay.get_scanner_options(options)
+ tokens = CodeRay.scan code, lang, scanner_options
+ encode_tokens tokens, options
+ end
+
+ # You can use highlight instead of encode, if that seems
+ # more clear to you.
+ alias highlight encode
+
+ # Encode the given +code+ using the Scanner for +lang+ in
+ # streaming mode.
+ def encode_stream code, lang, options = {}
+ raise NotStreamableError, self unless kind_of? Streamable
+ options = @options.merge options
+ setup options
+ scanner_options = CodeRay.get_scanner_options options
+ @token_stream =
+ CodeRay.scan_stream code, lang, scanner_options, &self
+ finish options
+ end
+
+ # Behave like a proc. The token method is converted to a proc.
+ def to_proc
+ method(:token).to_proc
+ end
+
+ # Return the default file extension for outputs of this encoder.
+ def file_extension
+ self.class::FILE_EXTENSION
+ end
+
+ protected
+
+ # Called with merged options before encoding starts.
+ # Sets @out to an empty string.
+ #
+ # See the HTML Encoder for an example of option caching.
+ def setup options
+ @out = ''
+ end
+
+ # Called with +text+ and +kind+ of the currently scanned token.
+ # For simple scanners, it's enougth to implement this method.
+ #
+ # By default, it calls text_token or block_token, depending on
+ # whether +text+ is a String.
+ def token text, kind
+ out =
+ if text.is_a? ::String # Ruby 1.9: :open.is_a? String
+ text_token text, kind
+ elsif text.is_a? ::Symbol
+ block_token text, kind
+ else
+ raise 'Unknown token text type: %p' % text
+ end
+ @out << out if @out
+ end
+
+ def text_token text, kind
+ end
+
+ def block_token action, kind
+ case action
+ when :open
+ open_token kind
+ when :close
+ close_token kind
+ else
+ raise 'unknown block action: %p' % action
+ end
+ end
+
+ # Called with merged options after encoding starts.
+ # The return value is the result of encoding, typically @out.
+ def finish options
+ @out
+ end
+
+ # Do the encoding.
+ #
+ # The already created +tokens+ object must be used; it can be a
+ # TokenStream or a Tokens object.
+ def compile tokens, options
+ tokens.each(&self)
+ end
+
+ end
+
+ end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ map :stats => :statistic,
+ :plain => :text,
+ :tex => :latex
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ class Count < Encoder
+
+ include Streamable
+ register_for :count
+
+ protected
+
+ def setup options
+ @out = 0
+ end
+
+ def token text, kind
+ @out += 1
+ end
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # = Debug Encoder
+ #
+ # Fast encoder producing simple debug output.
+ #
+ # It is readable and diff-able and is used for testing.
+ #
+ # You cannot fully restore the tokens information from the
+ # output, because consecutive :space tokens are merged.
+ # Use Tokens#dump for caching purposes.
+ class Debug < Encoder
+
+ include Streamable
+ register_for :debug
+
+ FILE_EXTENSION = 'raydebug'
+
+ protected
+ def text_token text, kind
+ if kind == :space
+ text
+ else
+ text = text.gsub(/[)\\]/, '\\\\\0') # escape ) and \
+ "#{kind}(#{text})"
+ end
+ end
+
+ def open_token kind
+ "#{kind}<"
+ end
+
+ def close_token kind
+ ">"
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ load :html
+
+ class Div < HTML
+
+ FILE_EXTENSION = 'div.html'
+
+ register_for :div
+
+ DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
+ :css => :style,
+ :wrap => :div,
+ })
+
+ end
+
+end
+end
--- /dev/null
+require "set"
+
+module CodeRay
+module Encoders
+
+ # = HTML Encoder
+ #
+ # This is CodeRay's most important highlighter:
+ # It provides save, fast XHTML generation and CSS support.
+ #
+ # == Usage
+ #
+ # require 'coderay'
+ # puts CodeRay.scan('Some /code/', :ruby).html #-> a HTML page
+ # puts CodeRay.scan('Some /code/', :ruby).html(:wrap => :span)
+ # #-> <span class="CodeRay"><span class="co">Some</span> /code/</span>
+ # puts CodeRay.scan('Some /code/', :ruby).span #-> the same
+ #
+ # puts CodeRay.scan('Some code', :ruby).html(
+ # :wrap => nil,
+ # :line_numbers => :inline,
+ # :css => :style
+ # )
+ # #-> <span class="no">1</span> <span style="color:#036; font-weight:bold;">Some</span> code
+ #
+ # == Options
+ #
+ # === :escape
+ # Escape html entities
+ # Default: true
+ #
+ # === :tab_width
+ # Convert \t characters to +n+ spaces (a number.)
+ # Default: 8
+ #
+ # === :css
+ # How to include the styles; can be :class or :style.
+ #
+ # Default: :class
+ #
+ # === :wrap
+ # Wrap in :page, :div, :span or nil.
+ #
+ # You can also use Encoders::Div and Encoders::Span.
+ #
+ # Default: nil
+ #
+ # === :line_numbers
+ # Include line numbers in :table, :inline, :list or nil (no line numbers)
+ #
+ # Default: nil
+ #
+ # === :line_number_start
+ # Where to start with line number counting.
+ #
+ # Default: 1
+ #
+ # === :bold_every
+ # Make every +n+-th number appear bold.
+ #
+ # Default: 10
+ #
+ # === :hint
+ # Include some information into the output using the title attribute.
+ # Can be :info (show token type on mouse-over), :info_long (with full path)
+ # or :debug (via inspect).
+ #
+ # Default: false
+ class HTML < Encoder
+
+ include Streamable
+ register_for :html
+
+ FILE_EXTENSION = 'html'
+
+ DEFAULT_OPTIONS = {
+ :escape => true,
+ :tab_width => 8,
+
+ :level => :xhtml,
+ :css => :class,
+
+ :style => :cycnus,
+
+ :wrap => nil,
+
+ :line_numbers => nil,
+ :line_number_start => 1,
+ :bold_every => 10,
+
+ :hint => false,
+ }
+
+ helper :output, :css
+
+ attr_reader :css
+
+ protected
+
+ HTML_ESCAPE = { #:nodoc:
+ '&' => '&',
+ '"' => '"',
+ '>' => '>',
+ '<' => '<',
+ }
+
+ # This was to prevent illegal HTML.
+ # Strange chars should still be avoided in codes.
+ evil_chars = Array(0x00...0x20) - [?\n, ?\t, ?\s]
+ evil_chars.each { |i| HTML_ESCAPE[i.chr] = ' ' }
+ #ansi_chars = Array(0x7f..0xff)
+ #ansi_chars.each { |i| HTML_ESCAPE[i.chr] = '&#%d;' % i }
+ # \x9 (\t) and \xA (\n) not included
+ #HTML_ESCAPE_PATTERN = /[\t&"><\0-\x8\xB-\x1f\x7f-\xff]/
+ HTML_ESCAPE_PATTERN = /[\t"&><\0-\x8\xB-\x1f]/
+
+ TOKEN_KIND_TO_INFO = Hash.new { |h, kind|
+ h[kind] =
+ case kind
+ when :pre_constant
+ 'Predefined constant'
+ else
+ kind.to_s.gsub(/_/, ' ').gsub(/\b\w/) { $&.capitalize }
+ end
+ }
+
+ TRANSPARENT_TOKEN_KINDS = [
+ :delimiter, :modifier, :content, :escape, :inline_delimiter,
+ ].to_set
+
+ # Generate a hint about the given +classes+ in a +hint+ style.
+ #
+ # +hint+ may be :info, :info_long or :debug.
+ def self.token_path_to_hint hint, classes
+ title =
+ case hint
+ when :info
+ TOKEN_KIND_TO_INFO[classes.first]
+ when :info_long
+ classes.reverse.map { |kind| TOKEN_KIND_TO_INFO[kind] }.join('/')
+ when :debug
+ classes.inspect
+ end
+ " title=\"#{title}\""
+ end
+
+ def setup options
+ super
+
+ @HTML_ESCAPE = HTML_ESCAPE.dup
+ @HTML_ESCAPE["\t"] = ' ' * options[:tab_width]
+
+ @escape = options[:escape]
+ @opened = [nil]
+ @css = CSS.new options[:style]
+
+ hint = options[:hint]
+ if hint and not [:debug, :info, :info_long].include? hint
+ raise ArgumentError, "Unknown value %p for :hint; \
+ expected :info, :debug, false, or nil." % hint
+ end
+
+ case options[:css]
+
+ when :class
+ @css_style = Hash.new do |h, k|
+ c = Tokens::ClassOfKind[k.first]
+ if c == :NO_HIGHLIGHT and not hint
+ h[k.dup] = false
+ else
+ title = if hint
+ HTML.token_path_to_hint(hint, k[1..-1] << k.first)
+ else
+ ''
+ end
+ if c == :NO_HIGHLIGHT
+ h[k.dup] = '<span%s>' % [title]
+ else
+ h[k.dup] = '<span%s class="%s">' % [title, c]
+ end
+ end
+ end
+
+ when :style
+ @css_style = Hash.new do |h, k|
+ if k.is_a? ::Array
+ styles = k.dup
+ else
+ styles = [k]
+ end
+ type = styles.first
+ classes = styles.map { |c| Tokens::ClassOfKind[c] }
+ if classes.first == :NO_HIGHLIGHT and not hint
+ h[k] = false
+ else
+ styles.shift if TRANSPARENT_TOKEN_KINDS.include? styles.first
+ title = HTML.token_path_to_hint hint, styles
+ style = @css[*classes]
+ h[k] =
+ if style
+ '<span%s style="%s">' % [title, style]
+ else
+ false
+ end
+ end
+ end
+
+ else
+ raise ArgumentError, "Unknown value %p for :css." % options[:css]
+
+ end
+ end
+
+ def finish options
+ not_needed = @opened.shift
+ @out << '</span>' * @opened.size
+ unless @opened.empty?
+ warn '%d tokens still open: %p' % [@opened.size, @opened]
+ end
+
+ @out.extend Output
+ @out.css = @css
+ @out.numerize! options[:line_numbers], options
+ @out.wrap! options[:wrap]
+
+ super
+ end
+
+ def token text, type
+ if text.is_a? ::String
+ if @escape && (text =~ /#{HTML_ESCAPE_PATTERN}/o)
+ text = text.gsub(/#{HTML_ESCAPE_PATTERN}/o) { |m| @HTML_ESCAPE[m] }
+ end
+ @opened[0] = type
+ if style = @css_style[@opened]
+ @out << style << text << '</span>'
+ else
+ @out << text
+ end
+ else
+ case text
+ when :open
+ @opened[0] = type
+ @out << (@css_style[@opened] || '<span>')
+ @opened << type
+ when :close
+ if @opened.empty?
+ # nothing to close
+ else
+ if $DEBUG and (@opened.size == 1 or @opened.last != type)
+ raise 'Malformed token stream: Trying to close a token (%p) \
+ that is not open. Open are: %p.' % [type, @opened[1..-1]]
+ end
+ @out << '</span>'
+ @opened.pop
+ end
+ when nil
+ raise 'Token with nil as text was given: %p' % [[text, type]]
+ else
+ raise 'unknown token kind: %p' % text
+ end
+ end
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ class HTML
+ class CSS
+
+ attr :stylesheet
+
+ def CSS.load_stylesheet style = nil
+ CodeRay::Styles[style]
+ end
+
+ def initialize style = :default
+ @classes = Hash.new
+ style = CSS.load_stylesheet style
+ @stylesheet = [
+ style::CSS_MAIN_STYLES,
+ style::TOKEN_COLORS.gsub(/^(?!$)/, '.CodeRay ')
+ ].join("\n")
+ parse style::TOKEN_COLORS
+ end
+
+ def [] *styles
+ cl = @classes[styles.first]
+ return '' unless cl
+ style = ''
+ 1.upto(styles.size) do |offset|
+ break if style = cl[styles[offset .. -1]]
+ end
+ raise 'Style not found: %p' % [styles] if $DEBUG and style.empty?
+ return style
+ end
+
+ private
+
+ CSS_CLASS_PATTERN = /
+ ( (?: # $1 = classes
+ \s* \. [-\w]+
+ )+ )
+ \s* \{ \s*
+ ( [^\}]+ )? # $2 = style
+ \s* \} \s*
+ |
+ ( . ) # $3 = error
+ /mx
+ def parse stylesheet
+ stylesheet.scan CSS_CLASS_PATTERN do |classes, style, error|
+ raise "CSS parse error: '#{error.inspect}' not recognized" if error
+ styles = classes.scan(/[-\w]+/)
+ cl = styles.pop
+ @classes[cl] ||= Hash.new
+ @classes[cl][styles] = style.to_s.strip
+ end
+ end
+
+ end
+ end
+
+end
+end
+
+if $0 == __FILE__
+ require 'pp'
+ pp CodeRay::Encoders::HTML::CSS.new
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ class HTML
+
+ module Output
+
+ def numerize *args
+ clone.numerize!(*args)
+ end
+
+=begin NUMERIZABLE_WRAPPINGS = {
+ :table => [:div, :page, nil],
+ :inline => :all,
+ :list => [:div, :page, nil]
+ }
+ NUMERIZABLE_WRAPPINGS.default = :all
+=end
+ def numerize! mode = :table, options = {}
+ return self unless mode
+
+ options = DEFAULT_OPTIONS.merge options
+
+ start = options[:line_number_start]
+ unless start.is_a? Integer
+ raise ArgumentError, "Invalid value %p for :line_number_start; Integer expected." % start
+ end
+
+ #allowed_wrappings = NUMERIZABLE_WRAPPINGS[mode]
+ #unless allowed_wrappings == :all or allowed_wrappings.include? options[:wrap]
+ # raise ArgumentError, "Can't numerize, :wrap must be in %p, but is %p" % [NUMERIZABLE_WRAPPINGS, options[:wrap]]
+ #end
+
+ bold_every = options[:bold_every]
+ bolding =
+ if bold_every == false
+ proc { |line| line.to_s }
+ elsif bold_every.is_a? Integer
+ raise ArgumentError, ":bolding can't be 0." if bold_every == 0
+ proc do |line|
+ if line % bold_every == 0
+ "<strong>#{line}</strong>" # every bold_every-th number in bold
+ else
+ line.to_s
+ end
+ end
+ else
+ raise ArgumentError, 'Invalid value %p for :bolding; false or Integer expected.' % bold_every
+ end
+
+ case mode
+ when :inline
+ max_width = (start + line_count).to_s.size
+ line = start
+ gsub!(/^/) do
+ line_number = bolding.call line
+ indent = ' ' * (max_width - line.to_s.size)
+ res = "<span class=\"no\">#{indent}#{line_number}</span> "
+ line += 1
+ res
+ end
+
+ when :table
+ # This is really ugly.
+ # Because even monospace fonts seem to have different heights when bold,
+ # I make the newline bold, both in the code and the line numbers.
+ # FIXME Still not working perfect for Mr. Internet Exploder
+ # FIXME Firefox struggles with very long codes (> 200 lines)
+ line_numbers = (start ... start + line_count).to_a.map(&bolding).join("\n")
+ line_numbers << "\n" # also for Mr. MS Internet Exploder :-/
+ line_numbers.gsub!(/\n/) { "<tt>\n</tt>" }
+
+ line_numbers_table_tpl = TABLE.apply('LINE_NUMBERS', line_numbers)
+ gsub!(/\n/) { "<tt>\n</tt>" }
+ wrap_in! line_numbers_table_tpl
+ @wrapped_in = :div
+
+ when :list
+ opened_tags = []
+ gsub!(/^.*$\n?/) do |line|
+ line.chomp!
+
+ open = opened_tags.join
+ line.scan(%r!<(/)?span[^>]*>?!) do |close,|
+ if close
+ opened_tags.pop
+ else
+ opened_tags << $&
+ end
+ end
+ close = '</span>' * opened_tags.size
+
+ "<li>#{open}#{line}#{close}</li>"
+ end
+ wrap_in! LIST
+ @wrapped_in = :div
+
+ else
+ raise ArgumentError, 'Unknown value %p for mode: expected one of %p' %
+ [mode, [:table, :list, :inline]]
+ end
+
+ self
+ end
+
+ def line_count
+ line_count = count("\n")
+ position_of_last_newline = rindex(?\n)
+ if position_of_last_newline
+ after_last_newline = self[position_of_last_newline + 1 .. -1]
+ ends_with_newline = after_last_newline[/\A(?:<\/span>)*\z/]
+ line_count += 1 if not ends_with_newline
+ end
+ line_count
+ end
+
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ class HTML
+
+ # This module is included in the output String from thew HTML Encoder.
+ #
+ # It provides methods like wrap, div, page etc.
+ #
+ # Remember to use #clone instead of #dup to keep the modules the object was
+ # extended with.
+ #
+ # TODO: more doc.
+ module Output
+
+ require 'coderay/encoders/html/numerization.rb'
+
+ attr_accessor :css
+
+ class << self
+
+ # This makes Output look like a class.
+ #
+ # Example:
+ #
+ # a = Output.new '<span class="co">Code</span>'
+ # a.wrap! :page
+ def new string, css = CSS.new, element = nil
+ output = string.clone.extend self
+ output.wrapped_in = element
+ output.css = css
+ output
+ end
+
+ # Raises an exception if an object that doesn't respond to to_str is extended by Output,
+ # to prevent users from misuse. Use Module#remove_method to disable.
+ def extended o
+ warn "The Output module is intended to extend instances of String, not #{o.class}." unless o.respond_to? :to_str
+ end
+
+ def make_stylesheet css, in_tag = false
+ sheet = css.stylesheet
+ sheet = <<-CSS if in_tag
+<style type="text/css">
+#{sheet}
+</style>
+ CSS
+ sheet
+ end
+
+ def page_template_for_css css
+ sheet = make_stylesheet css
+ PAGE.apply 'CSS', sheet
+ end
+
+ # Define a new wrapper. This is meta programming.
+ def wrapper *wrappers
+ wrappers.each do |wrapper|
+ define_method wrapper do |*args|
+ wrap wrapper, *args
+ end
+ define_method "#{wrapper}!".to_sym do |*args|
+ wrap! wrapper, *args
+ end
+ end
+ end
+
+ end
+
+ wrapper :div, :span, :page
+
+ def wrapped_in? element
+ wrapped_in == element
+ end
+
+ def wrapped_in
+ @wrapped_in ||= nil
+ end
+ attr_writer :wrapped_in
+
+ def wrap_in template
+ clone.wrap_in! template
+ end
+
+ def wrap_in! template
+ Template.wrap! self, template, 'CONTENT'
+ self
+ end
+
+ def wrap! element, *args
+ return self if not element or element == wrapped_in
+ case element
+ when :div
+ raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? nil
+ wrap_in! DIV
+ when :span
+ raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? nil
+ wrap_in! SPAN
+ when :page
+ wrap! :div if wrapped_in? nil
+ raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? :div
+ wrap_in! Output.page_template_for_css(@css)
+ when nil
+ return self
+ else
+ raise "Unknown value %p for :wrap" % element
+ end
+ @wrapped_in = element
+ self
+ end
+
+ def wrap *args
+ clone.wrap!(*args)
+ end
+
+ def stylesheet in_tag = false
+ Output.make_stylesheet @css, in_tag
+ end
+
+ class Template < String
+
+ def self.wrap! str, template, target
+ target = Regexp.new(Regexp.escape("<%#{target}%>"))
+ if template =~ target
+ str[0,0] = $`
+ str << $'
+ else
+ raise "Template target <%%%p%%> not found" % target
+ end
+ end
+
+ def apply target, replacement
+ target = Regexp.new(Regexp.escape("<%#{target}%>"))
+ if self =~ target
+ Template.new($` + replacement + $')
+ else
+ raise "Template target <%%%p%%> not found" % target
+ end
+ end
+
+ module Simple
+ def ` str #` <-- for stupid editors
+ Template.new str
+ end
+ end
+ end
+
+ extend Template::Simple
+
+#-- don't include the templates in docu
+
+ SPAN = `<span class="CodeRay"><%CONTENT%></span>`
+
+ DIV = <<-`DIV`
+<div class="CodeRay">
+ <div class="code"><pre><%CONTENT%></pre></div>
+</div>
+ DIV
+
+ TABLE = <<-`TABLE`
+<table class="CodeRay"><tr>
+ <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre><%LINE_NUMBERS%></pre></td>
+ <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><%CONTENT%></pre></td>
+</tr></table>
+ TABLE
+ # title="double click to expand"
+
+ LIST = <<-`LIST`
+<ol class="CodeRay"><%CONTENT%></ol>
+ LIST
+
+ PAGE = <<-`PAGE`
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="de">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>CodeRay HTML Encoder Example</title>
+ <style type="text/css">
+<%CSS%>
+ </style>
+</head>
+<body style="background-color: white;">
+
+<%CONTENT%>
+</body>
+</html>
+ PAGE
+
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # = Null Encoder
+ #
+ # Does nothing and returns an empty string.
+ class Null < Encoder
+
+ include Streamable
+ register_for :null
+
+ # Defined for faster processing
+ def to_proc
+ proc {}
+ end
+
+ protected
+
+ def token(*)
+ # do nothing
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ load :html
+
+ class Page < HTML
+
+ FILE_EXTENSION = 'html'
+
+ register_for :page
+
+ DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
+ :css => :class,
+ :wrap => :page,
+ :line_numbers => :table
+ })
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ load :html
+
+ class Span < HTML
+
+ FILE_EXTENSION = 'span.html'
+
+ register_for :span
+
+ DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
+ :css => :style,
+ :wrap => :span,
+ })
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # Makes a statistic for the given tokens.
+ class Statistic < Encoder
+
+ include Streamable
+ register_for :stats, :statistic
+
+ attr_reader :type_stats, :real_token_count
+
+ protected
+
+ TypeStats = Struct.new :count, :size
+
+ def setup options
+ @type_stats = Hash.new { |h, k| h[k] = TypeStats.new 0, 0 }
+ @real_token_count = 0
+ end
+
+ def generate tokens, options
+ @tokens = tokens
+ super
+ end
+
+ def text_token text, kind
+ @real_token_count += 1 unless kind == :space
+ @type_stats[kind].count += 1
+ @type_stats[kind].size += text.size
+ @type_stats['TOTAL'].size += text.size
+ @type_stats['TOTAL'].count += 1
+ end
+
+ # TODO Hierarchy handling
+ def block_token action, kind
+ @type_stats['TOTAL'].count += 1
+ @type_stats['open/close'].count += 1
+ end
+
+ STATS = <<-STATS
+
+Code Statistics
+
+Tokens %8d
+ Non-Whitespace %8d
+Bytes Total %8d
+
+Token Types (%d):
+ type count ratio size (average)
+-------------------------------------------------------------
+%s
+ STATS
+# space 12007 33.81 % 1.7
+ TOKEN_TYPES_ROW = <<-TKR
+ %-20s %8d %6.2f %% %5.1f
+ TKR
+
+ def finish options
+ all = @type_stats['TOTAL']
+ all_count, all_size = all.count, all.size
+ @type_stats.each do |type, stat|
+ stat.size /= stat.count.to_f
+ end
+ types_stats = @type_stats.sort_by { |k, v| [-v.count, k.to_s] }.map do |k, v|
+ TOKEN_TYPES_ROW % [k, v.count, 100.0 * v.count / all_count, v.size]
+ end.join
+ STATS % [
+ all_count, @real_token_count, all_size,
+ @type_stats.delete_if { |k, v| k.is_a? String }.size,
+ types_stats
+ ]
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ class Text < Encoder
+
+ include Streamable
+ register_for :text
+
+ FILE_EXTENSION = 'txt'
+
+ DEFAULT_OPTIONS = {
+ :separator => ''
+ }
+
+ protected
+ def setup options
+ @out = ''
+ @sep = options[:separator]
+ end
+
+ def token text, kind
+ @out << text + @sep if text.is_a? ::String
+ end
+
+ def finish options
+ @out.chomp @sep
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # The Tokens encoder converts the tokens to a simple
+ # readable format. It doesn't use colors and is mainly
+ # intended for console output.
+ #
+ # The tokens are converted with Tokens.write_token.
+ #
+ # The format is:
+ #
+ # <token-kind> \t <escaped token-text> \n
+ #
+ # Example:
+ #
+ # require 'coderay'
+ # puts CodeRay.scan("puts 3 + 4", :ruby).tokens
+ #
+ # prints:
+ #
+ # ident puts
+ # space
+ # integer 3
+ # space
+ # operator +
+ # space
+ # integer 4
+ #
+ class Tokens < Encoder
+
+ include Streamable
+ register_for :tokens
+
+ FILE_EXTENSION = 'tok'
+
+ protected
+ def token text, kind
+ @out << CodeRay::Tokens.write_token(text, kind)
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # = XML Encoder
+ #
+ # Uses REXML. Very slow.
+ class XML < Encoder
+
+ include Streamable
+ register_for :xml
+
+ FILE_EXTENSION = 'xml'
+
+ require 'rexml/document'
+
+ DEFAULT_OPTIONS = {
+ :tab_width => 8,
+ :pretty => -1,
+ :transitive => false,
+ }
+
+ protected
+
+ def setup options
+ @doc = REXML::Document.new
+ @doc << REXML::XMLDecl.new
+ @tab_width = options[:tab_width]
+ @root = @node = @doc.add_element('coderay-tokens')
+ end
+
+ def finish options
+ @doc.write @out, options[:pretty], options[:transitive], true
+ @out
+ end
+
+ def text_token text, kind
+ if kind == :space
+ token = @node
+ else
+ token = @node.add_element kind.to_s
+ end
+ text.scan(/(\x20+)|(\t+)|(\n)|[^\x20\t\n]+/) do |space, tab, nl|
+ case
+ when space
+ token << REXML::Text.new(space, true)
+ when tab
+ token << REXML::Text.new(tab, true)
+ when nl
+ token << REXML::Text.new(nl, true)
+ else
+ token << REXML::Text.new($&)
+ end
+ end
+ end
+
+ def open_token kind
+ @node = @node.add_element kind.to_s
+ end
+
+ def close_token kind
+ if @node == @root
+ raise 'no token to close!'
+ end
+ @node = @node.parent
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Encoders
+
+ # = YAML Encoder
+ #
+ # Slow.
+ class YAML < Encoder
+
+ register_for :yaml
+
+ FILE_EXTENSION = 'yaml'
+
+ protected
+ def compile tokens, options
+ require 'yaml'
+ @out = tokens.to_a.to_yaml
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+
+# = FileType
+#
+# A simple filetype recognizer.
+#
+# Copyright (c) 2006 by murphy (Kornelius Kalnbach) <murphy rubychan de>
+#
+# License:: LGPL / ask the author
+# Version:: 0.1 (2005-09-01)
+#
+# == Documentation
+#
+# # determine the type of the given
+# lang = FileType[ARGV.first]
+#
+# # return :plaintext if the file type is unknown
+# lang = FileType.fetch ARGV.first, :plaintext
+#
+# # try the shebang line, too
+# lang = FileType.fetch ARGV.first, :plaintext, true
+module FileType
+
+ UnknownFileType = Class.new Exception
+
+ class << self
+
+ # Try to determine the file type of the file.
+ #
+ # +filename+ is a relative or absolute path to a file.
+ #
+ # The file itself is only accessed when +read_shebang+ is set to true.
+ # That means you can get filetypes from files that don't exist.
+ def [] filename, read_shebang = false
+ name = File.basename filename
+ ext = File.extname name
+ ext.sub!(/^\./, '') # delete the leading dot
+
+ type =
+ TypeFromExt[ext] ||
+ TypeFromExt[ext.downcase] ||
+ TypeFromName[name] ||
+ TypeFromName[name.downcase]
+ type ||= shebang(filename) if read_shebang
+
+ type
+ end
+
+ def shebang filename
+ begin
+ File.open filename, 'r' do |f|
+ first_line = f.gets
+ first_line[TypeFromShebang]
+ end
+ rescue IOError
+ nil
+ end
+ end
+
+ # This works like Hash#fetch.
+ #
+ # If the filetype cannot be found, the +default+ value
+ # is returned.
+ def fetch filename, default = nil, read_shebang = false
+ if default and block_given?
+ warn 'block supersedes default value argument'
+ end
+
+ unless type = self[filename, read_shebang]
+ return yield if block_given?
+ return default if default
+ raise UnknownFileType, 'Could not determine type of %p.' % filename
+ end
+ type
+ end
+
+ end
+
+ TypeFromExt = {
+ 'rb' => :ruby,
+ 'rbw' => :ruby,
+ 'rake' => :ruby,
+ 'mab' => :ruby,
+ 'cpp' => :c,
+ 'c' => :c,
+ 'h' => :c,
+ 'java' => :java,
+ 'js' => :javascript,
+ 'xml' => :xml,
+ 'htm' => :html,
+ 'html' => :html,
+ 'php' => :php,
+ 'php3' => :php,
+ 'php4' => :php,
+ 'php5' => :php,
+ 'xhtml' => :xhtml,
+ 'raydebug' => :debug,
+ 'rhtml' => :rhtml,
+ 'ss' => :scheme,
+ 'sch' => :scheme,
+ 'yaml' => :yaml,
+ 'yml' => :yaml,
+ }
+
+ TypeFromShebang = /\b(?:ruby|perl|python|sh)\b/
+
+ TypeFromName = {
+ 'Rakefile' => :ruby,
+ 'Rantfile' => :ruby,
+ }
+
+end
+
+end
+
+if $0 == __FILE__
+ $VERBOSE = true
+ eval DATA.read, nil, $0, __LINE__+4
+end
+
+__END__
+
+require 'test/unit'
+
+class TC_FileType < Test::Unit::TestCase
+
+ def test_fetch
+ assert_raise FileType::UnknownFileType do
+ FileType.fetch ''
+ end
+
+ assert_throws :not_found do
+ FileType.fetch '.' do
+ throw :not_found
+ end
+ end
+
+ assert_equal :default, FileType.fetch('c', :default)
+
+ stderr, fake_stderr = $stderr, Object.new
+ $err = ''
+ def fake_stderr.write x
+ $err << x
+ end
+ $stderr = fake_stderr
+ FileType.fetch('c', :default) { }
+ assert_equal "block supersedes default value argument\n", $err
+ $stderr = stderr
+ end
+
+ def test_ruby
+ assert_equal :ruby, FileType['test.rb']
+ assert_equal :ruby, FileType['C:\\Program Files\\x\\y\\c\\test.rbw']
+ assert_equal :ruby, FileType['/usr/bin/something/Rakefile']
+ assert_equal :ruby, FileType['~/myapp/gem/Rantfile']
+ assert_equal :ruby, FileType['./lib/tasks\repository.rake']
+ assert_not_equal :ruby, FileType['test_rb']
+ assert_not_equal :ruby, FileType['Makefile']
+ assert_not_equal :ruby, FileType['set.rb/set']
+ assert_not_equal :ruby, FileType['~/projects/blabla/rb']
+ end
+
+ def test_c
+ assert_equal :c, FileType['test.c']
+ assert_equal :c, FileType['C:\\Program Files\\x\\y\\c\\test.h']
+ assert_not_equal :c, FileType['test_c']
+ assert_not_equal :c, FileType['Makefile']
+ assert_not_equal :c, FileType['set.h/set']
+ assert_not_equal :c, FileType['~/projects/blabla/c']
+ end
+
+ def test_html
+ assert_equal :html, FileType['test.htm']
+ assert_equal :xhtml, FileType['test.xhtml']
+ assert_equal :xhtml, FileType['test.html.xhtml']
+ assert_equal :rhtml, FileType['_form.rhtml']
+ end
+
+ def test_yaml
+ assert_equal :yaml, FileType['test.yml']
+ assert_equal :yaml, FileType['test.yaml']
+ assert_equal :yaml, FileType['my.html.yaml']
+ assert_not_equal :yaml, FileType['YAML']
+ end
+
+ def test_shebang
+ dir = './test'
+ if File.directory? dir
+ Dir.chdir dir do
+ assert_equal :c, FileType['test.c']
+ end
+ end
+ end
+
+end
--- /dev/null
+# =GZip Simple
+#
+# A simplified interface to the gzip library +zlib+ (from the Ruby Standard Library.)
+#
+# Author: murphy (mail to murphy cYcnus de)
+#
+# Version: 0.2 (2005.may.28)
+#
+# ==Documentation
+#
+# See +GZip+ module and the +String+ extensions.
+#
+module GZip
+
+ require 'zlib'
+
+ # The default zipping level. 7 zips good and fast.
+ DEFAULT_GZIP_LEVEL = 7
+
+ # Unzips the given string +s+.
+ #
+ # Example:
+ # require 'gzip_simple'
+ # print GZip.gunzip(File.read('adresses.gz'))
+ def GZip.gunzip s
+ Zlib::Inflate.inflate s
+ end
+
+ # Zips the given string +s+.
+ #
+ # Example:
+ # require 'gzip_simple'
+ # File.open('adresses.gz', 'w') do |file
+ # file.write GZip.gzip('Mum: 0123 456 789', 9)
+ # end
+ #
+ # If you provide a +level+, you can control how strong
+ # the string is compressed:
+ # - 0: no compression, only convert to gzip format
+ # - 1: compress fast
+ # - 7: compress more, but still fast (default)
+ # - 8: compress more, slower
+ # - 9: compress best, very slow
+ def GZip.gzip s, level = DEFAULT_GZIP_LEVEL
+ Zlib::Deflate.new(level).deflate s, Zlib::FINISH
+ end
+end
+
+
+# String extensions to use the GZip module.
+#
+# The methods gzip and gunzip provide an even more simple
+# interface to the ZLib:
+#
+# # create a big string
+# x = 'a' * 1000
+#
+# # zip it
+# x_gz = x.gzip
+#
+# # test the result
+# puts 'Zipped %d bytes to %d bytes.' % [x.size, x_gz.size]
+# #-> Zipped 1000 bytes to 19 bytes.
+#
+# # unzipping works
+# p x_gz.gunzip == x #-> true
+class String
+ # Returns the string, unzipped.
+ # See GZip.gunzip
+ def gunzip
+ GZip.gunzip self
+ end
+ # Replaces the string with its unzipped value.
+ # See GZip.gunzip
+ def gunzip!
+ replace gunzip
+ end
+
+ # Returns the string, zipped.
+ # +level+ is the gzip compression level, see GZip.gzip.
+ def gzip level = GZip::DEFAULT_GZIP_LEVEL
+ GZip.gzip self, level
+ end
+ # Replaces the string with its zipped value.
+ # See GZip.gzip.
+ def gzip!(*args)
+ replace gzip(*args)
+ end
+end
+
+if $0 == __FILE__
+ eval DATA.read, nil, $0, __LINE__+4
+end
+
+__END__
+#CODE
+
+# Testing / Benchmark
+x = 'a' * 1000
+x_gz = x.gzip
+puts 'Zipped %d bytes to %d bytes.' % [x.size, x_gz.size] #-> Zipped 1000 bytes to 19 bytes.
+p x_gz.gunzip == x #-> true
+
+require 'benchmark'
+
+INFO = 'packed to %0.3f%%' # :nodoc:
+
+x = Array.new(100000) { rand(255).chr + 'aaaaaaaaa' + rand(255).chr }.join
+Benchmark.bm(10) do |bm|
+ for level in 0..9
+ bm.report "zip #{level}" do
+ $x = x.gzip level
+ end
+ puts INFO % [100.0 * $x.size / x.size]
+ end
+ bm.report 'zip' do
+ $x = x.gzip
+ end
+ puts INFO % [100.0 * $x.size / x.size]
+ bm.report 'unzip' do
+ $x.gunzip
+ end
+end
--- /dev/null
+module CodeRay
+
+# = PluginHost
+#
+# $Id: plugin.rb 220 2007-01-01 02:58:58Z murphy $
+#
+# A simple subclass plugin system.
+#
+# Example:
+# class Generators < PluginHost
+# plugin_path 'app/generators'
+# end
+#
+# class Generator
+# extend Plugin
+# PLUGIN_HOST = Generators
+# end
+#
+# class FancyGenerator < Generator
+# register_for :fancy
+# end
+#
+# Generators[:fancy] #-> FancyGenerator
+# # or
+# require_plugin 'Generators/fancy'
+module PluginHost
+
+ # Raised if Encoders::[] fails because:
+ # * a file could not be found
+ # * the requested Encoder is not registered
+ PluginNotFound = Class.new Exception
+ HostNotFound = Class.new Exception
+
+ PLUGIN_HOSTS = []
+ PLUGIN_HOSTS_BY_ID = {} # dummy hash
+
+ # Loads all plugins using list and load.
+ def load_all
+ for plugin in list
+ load plugin
+ end
+ end
+
+ # Returns the Plugin for +id+.
+ #
+ # Example:
+ # yaml_plugin = MyPluginHost[:yaml]
+ def [] id, *args, &blk
+ plugin = validate_id(id)
+ begin
+ plugin = plugin_hash.[] plugin, *args, &blk
+ end while plugin.is_a? Symbol
+ plugin
+ end
+
+ # Alias for +[]+.
+ alias load []
+
+ def require_helper plugin_id, helper_name
+ path = path_to File.join(plugin_id, helper_name)
+ require path
+ end
+
+ class << self
+
+ # Adds the module/class to the PLUGIN_HOSTS list.
+ def extended mod
+ PLUGIN_HOSTS << mod
+ end
+
+ # Warns you that you should not #include this module.
+ def included mod
+ warn "#{name} should not be included. Use extend."
+ end
+
+ # Find the PluginHost for host_id.
+ def host_by_id host_id
+ unless PLUGIN_HOSTS_BY_ID.default_proc
+ ph = Hash.new do |h, a_host_id|
+ for host in PLUGIN_HOSTS
+ h[host.host_id] = host
+ end
+ h.fetch a_host_id, nil
+ end
+ PLUGIN_HOSTS_BY_ID.replace ph
+ end
+ PLUGIN_HOSTS_BY_ID[host_id]
+ end
+
+ end
+
+ # The path where the plugins can be found.
+ def plugin_path *args
+ unless args.empty?
+ @plugin_path = File.expand_path File.join(*args)
+ load_map
+ end
+ @plugin_path
+ end
+
+ # The host's ID.
+ #
+ # If PLUGIN_HOST_ID is not set, it is simply the class name.
+ def host_id
+ if self.const_defined? :PLUGIN_HOST_ID
+ self::PLUGIN_HOST_ID
+ else
+ name
+ end
+ end
+
+ # Map a plugin_id to another.
+ #
+ # Usage: Put this in a file plugin_path/_map.rb.
+ #
+ # class MyColorHost < PluginHost
+ # map :navy => :dark_blue,
+ # :maroon => :brown,
+ # :luna => :moon
+ # end
+ def map hash
+ for from, to in hash
+ from = validate_id from
+ to = validate_id to
+ plugin_hash[from] = to unless plugin_hash.has_key? from
+ end
+ end
+
+ # Define the default plugin to use when no plugin is found
+ # for a given id.
+ #
+ # See also map.
+ #
+ # class MyColorHost < PluginHost
+ # map :navy => :dark_blue
+ # default :gray
+ # end
+ def default id
+ id = validate_id id
+ plugin_hash[nil] = id
+ end
+
+ # Every plugin must register itself for one or more
+ # +ids+ by calling register_for, which calls this method.
+ #
+ # See Plugin#register_for.
+ def register plugin, *ids
+ for id in ids
+ unless id.is_a? Symbol
+ raise ArgumentError,
+ "id must be a Symbol, but it was a #{id.class}"
+ end
+ plugin_hash[validate_id(id)] = plugin
+ end
+ end
+
+ # A Hash of plugion_id => Plugin pairs.
+ def plugin_hash
+ @plugin_hash ||= create_plugin_hash
+ end
+
+ # Returns an array of all .rb files in the plugin path.
+ #
+ # The extension .rb is not included.
+ def list
+ Dir[path_to('*')].select do |file|
+ File.basename(file)[/^(?!_)\w+\.rb$/]
+ end.map do |file|
+ File.basename file, '.rb'
+ end
+ end
+
+ # Makes a map of all loaded plugins.
+ def inspect
+ map = plugin_hash.dup
+ map.each do |id, plugin|
+ map[id] = plugin.to_s[/(?>[\w_]+)$/]
+ end
+ "#{name}[#{host_id}]#{map.inspect}"
+ end
+
+protected
+ # Created a new plugin list and stores it to @plugin_hash.
+ def create_plugin_hash
+ @plugin_hash =
+ Hash.new do |h, plugin_id|
+ id = validate_id(plugin_id)
+ path = path_to id
+ begin
+ require path
+ rescue LoadError => boom
+ if h.has_key? nil # default plugin
+ h[id] = h[nil]
+ else
+ raise PluginNotFound, 'Could not load plugin %p: %s' % [id, boom]
+ end
+ else
+ # Plugin should have registered by now
+ unless h.has_key? id
+ raise PluginNotFound,
+ "No #{self.name} plugin for #{id.inspect} found in #{path}."
+ end
+ end
+ h[id]
+ end
+ end
+
+ # Loads the map file (see map).
+ #
+ # This is done automatically when plugin_path is called.
+ def load_map
+ mapfile = path_to '_map'
+ if File.exist? mapfile
+ require mapfile
+ elsif $DEBUG
+ warn 'no _map.rb found for %s' % name
+ end
+ end
+
+ # Returns the Plugin for +id+.
+ # Use it like Hash#fetch.
+ #
+ # Example:
+ # yaml_plugin = MyPluginHost[:yaml, :default]
+ def fetch id, *args, &blk
+ plugin_hash.fetch validate_id(id), *args, &blk
+ end
+
+ # Returns the expected path to the plugin file for the given id.
+ def path_to plugin_id
+ File.join plugin_path, "#{plugin_id}.rb"
+ end
+
+ # Converts +id+ to a Symbol if it is a String,
+ # or returns +id+ if it already is a Symbol.
+ #
+ # Raises +ArgumentError+ for all other objects, or if the
+ # given String includes non-alphanumeric characters (\W).
+ def validate_id id
+ if id.is_a? Symbol or id.nil?
+ id
+ elsif id.is_a? String
+ if id[/\w+/] == id
+ id.to_sym
+ else
+ raise ArgumentError, "Invalid id: '#{id}' given."
+ end
+ else
+ raise ArgumentError,
+ "String or Symbol expected, but #{id.class} given."
+ end
+ end
+
+end
+
+
+# = Plugin
+#
+# Plugins have to include this module.
+#
+# IMPORTANT: use extend for this module.
+#
+# Example: see PluginHost.
+module Plugin
+
+ def included mod
+ warn "#{name} should not be included. Use extend."
+ end
+
+ # Register this class for the given langs.
+ # Example:
+ # class MyPlugin < PluginHost::BaseClass
+ # register_for :my_id
+ # ...
+ # end
+ #
+ # See PluginHost.register.
+ def register_for *ids
+ plugin_host.register self, *ids
+ end
+
+ # The host for this Plugin class.
+ def plugin_host host = nil
+ if host and not host.is_a? PluginHost
+ raise ArgumentError,
+ "PluginHost expected, but #{host.class} given."
+ end
+ self.const_set :PLUGIN_HOST, host if host
+ self::PLUGIN_HOST
+ end
+
+ # Require some helper files.
+ #
+ # Example:
+ #
+ # class MyPlugin < PluginHost::BaseClass
+ # register_for :my_id
+ # helper :my_helper
+ #
+ # The above example loads the file myplugin/my_helper.rb relative to the
+ # file in which MyPlugin was defined.
+ def helper *helpers
+ for helper in helpers
+ self::PLUGIN_HOST.require_helper plugin_id, helper.to_s
+ end
+ end
+
+ # Returns the pulgin id used by the engine.
+ def plugin_id
+ name[/[\w_]+$/].downcase
+ end
+
+end
+
+# Convenience method for plugin loading.
+# The syntax used is:
+#
+# CodeRay.require_plugin '<Host ID>/<Plugin ID>'
+#
+# Returns the loaded plugin.
+def require_plugin path
+ host_id, plugin_id = path.split '/', 2
+ host = PluginHost.host_by_id(host_id)
+ raise PluginHost::HostNotFound,
+ "No host for #{host_id.inspect} found." unless host
+ host.load plugin_id
+end
+
+end
\ No newline at end of file
--- /dev/null
+module CodeRay
+
+# = WordList
+#
+# <b>A Hash subclass designed for mapping word lists to token types.</b>
+#
+# Copyright (c) 2006 by murphy (Kornelius Kalnbach) <murphy rubychan de>
+#
+# License:: LGPL / ask the author
+# Version:: 1.1 (2006-Oct-19)
+#
+# A WordList is a Hash with some additional features.
+# It is intended to be used for keyword recognition.
+#
+# WordList is highly optimized to be used in Scanners,
+# typically to decide whether a given ident is a special token.
+#
+# For case insensitive words use CaseIgnoringWordList.
+#
+# Example:
+#
+# # define word arrays
+# RESERVED_WORDS = %w[
+# asm break case continue default do else
+# ...
+# ]
+#
+# PREDEFINED_TYPES = %w[
+# int long short char void
+# ...
+# ]
+#
+# PREDEFINED_CONSTANTS = %w[
+# EOF NULL ...
+# ]
+#
+# # make a WordList
+# IDENT_KIND = WordList.new(:ident).
+# add(RESERVED_WORDS, :reserved).
+# add(PREDEFINED_TYPES, :pre_type).
+# add(PREDEFINED_CONSTANTS, :pre_constant)
+#
+# ...
+#
+# def scan_tokens tokens, options
+# ...
+#
+# elsif scan(/[A-Za-z_][A-Za-z_0-9]*/)
+# # use it
+# kind = IDENT_KIND[match]
+# ...
+class WordList < Hash
+
+ # Creates a new WordList with +default+ as default value.
+ #
+ # You can activate +caching+ to store the results for every [] request.
+ #
+ # With caching, methods like +include?+ or +delete+ may no longer behave
+ # as you expect. Therefore, it is recommended to use the [] method only.
+ def initialize default = false, caching = false, &block
+ if block
+ raise ArgumentError, 'Can\'t combine block with caching.' if caching
+ super(&block)
+ else
+ if caching
+ super() do |h, k|
+ h[k] = h.fetch k, default
+ end
+ else
+ super default
+ end
+ end
+ end
+
+ # Add words to the list and associate them with +kind+.
+ #
+ # Returns +self+, so you can concat add calls.
+ def add words, kind = true
+ words.each do |word|
+ self[word] = kind
+ end
+ self
+ end
+
+end
+
+
+# A CaseIgnoringWordList is like a WordList, only that
+# keys are compared case-insensitively.
+#
+# Ignoring the text case is realized by sending the +downcase+ message to
+# all keys.
+#
+# Caching usually makes a CaseIgnoringWordList faster, but it has to be
+# activated explicitely.
+class CaseIgnoringWordList < WordList
+
+ # Creates a new case-insensitive WordList with +default+ as default value.
+ #
+ # You can activate caching to store the results for every [] request.
+ def initialize default = false, caching = false
+ if caching
+ super(default, false) do |h, k|
+ h[k] = h.fetch k.downcase, default
+ end
+ else
+ def self.[] key # :nodoc:
+ super(key.downcase)
+ end
+ end
+ end
+
+ # Add +words+ to the list and associate them with +kind+.
+ def add words, kind = true
+ words.each do |word|
+ self[word.downcase] = kind
+ end
+ self
+ end
+
+end
+
+end
\ No newline at end of file
--- /dev/null
+module CodeRay
+
+ require 'coderay/helpers/plugin'
+
+ # = Scanners
+ #
+ # $Id: scanner.rb 222 2007-01-01 16:26:17Z murphy $
+ #
+ # This module holds the Scanner class and its subclasses.
+ # For example, the Ruby scanner is named CodeRay::Scanners::Ruby
+ # can be found in coderay/scanners/ruby.
+ #
+ # Scanner also provides methods and constants for the register
+ # mechanism and the [] method that returns the Scanner class
+ # belonging to the given lang.
+ #
+ # See PluginHost.
+ module Scanners
+ extend PluginHost
+ plugin_path File.dirname(__FILE__), 'scanners'
+
+ require 'strscan'
+
+ # = Scanner
+ #
+ # The base class for all Scanners.
+ #
+ # It is a subclass of Ruby's great +StringScanner+, which
+ # makes it easy to access the scanning methods inside.
+ #
+ # It is also +Enumerable+, so you can use it like an Array of
+ # Tokens:
+ #
+ # require 'coderay'
+ #
+ # c_scanner = CodeRay::Scanners[:c].new "if (*p == '{') nest++;"
+ #
+ # for text, kind in c_scanner
+ # puts text if kind == :operator
+ # end
+ #
+ # # prints: (*==)++;
+ #
+ # OK, this is a very simple example :)
+ # You can also use +map+, +any?+, +find+ and even +sort_by+,
+ # if you want.
+ class Scanner < StringScanner
+ extend Plugin
+ plugin_host Scanners
+
+ # Raised if a Scanner fails while scanning
+ ScanError = Class.new(Exception)
+
+ require 'coderay/helpers/word_list'
+
+ # The default options for all scanner classes.
+ #
+ # Define @default_options for subclasses.
+ DEFAULT_OPTIONS = { :stream => false }
+
+ class << self
+
+ # Returns if the Scanner can be used in streaming mode.
+ def streamable?
+ is_a? Streamable
+ end
+
+ def normify code
+ code = code.to_s.to_unix
+ end
+
+ def file_extension extension = nil
+ if extension
+ @file_extension = extension.to_s
+ else
+ @file_extension ||= plugin_id.to_s
+ end
+ end
+
+ end
+
+=begin
+## Excluded for speed reasons; protected seems to make methods slow.
+
+ # Save the StringScanner methods from being called.
+ # This would not be useful for highlighting.
+ strscan_public_methods =
+ StringScanner.instance_methods -
+ StringScanner.ancestors[1].instance_methods
+ protected(*strscan_public_methods)
+=end
+
+ # Create a new Scanner.
+ #
+ # * +code+ is the input String and is handled by the superclass
+ # StringScanner.
+ # * +options+ is a Hash with Symbols as keys.
+ # It is merged with the default options of the class (you can
+ # overwrite default options here.)
+ # * +block+ is the callback for streamed highlighting.
+ #
+ # If you set :stream to +true+ in the options, the Scanner uses a
+ # TokenStream with the +block+ as callback to handle the tokens.
+ #
+ # Else, a Tokens object is used.
+ def initialize code='', options = {}, &block
+ @options = self.class::DEFAULT_OPTIONS.merge options
+ raise "I am only the basic Scanner class. I can't scan "\
+ "anything. :( Use my subclasses." if self.class == Scanner
+
+ super Scanner.normify(code)
+
+ @tokens = options[:tokens]
+ if @options[:stream]
+ warn "warning in CodeRay::Scanner.new: :stream is set, "\
+ "but no block was given" unless block_given?
+ raise NotStreamableError, self unless kind_of? Streamable
+ @tokens ||= TokenStream.new(&block)
+ else
+ warn "warning in CodeRay::Scanner.new: Block given, "\
+ "but :stream is #{@options[:stream]}" if block_given?
+ @tokens ||= Tokens.new
+ end
+
+ setup
+ end
+
+ def reset
+ super
+ reset_instance
+ end
+
+ def string= code
+ code = Scanner.normify(code)
+ super code
+ reset_instance
+ end
+
+ # More mnemonic accessor name for the input string.
+ alias code string
+ alias code= string=
+
+ # Scans the code and returns all tokens in a Tokens object.
+ def tokenize new_string=nil, options = {}
+ options = @options.merge(options)
+ self.string = new_string if new_string
+ @cached_tokens =
+ if @options[:stream] # :stream must have been set already
+ reset unless new_string
+ scan_tokens @tokens, options
+ @tokens
+ else
+ scan_tokens @tokens, options
+ end
+ end
+
+ def tokens
+ @cached_tokens ||= tokenize
+ end
+
+ # Whether the scanner is in streaming mode.
+ def streaming?
+ !!@options[:stream]
+ end
+
+ # Traverses the tokens.
+ def each &block
+ raise ArgumentError,
+ 'Cannot traverse TokenStream.' if @options[:stream]
+ tokens.each(&block)
+ end
+ include Enumerable
+
+ # The current line position of the scanner.
+ #
+ # Beware, this is implemented inefficiently. It should be used
+ # for debugging only.
+ def line
+ string[0..pos].count("\n") + 1
+ end
+
+ protected
+
+ # Can be implemented by subclasses to do some initialization
+ # that has to be done once per instance.
+ #
+ # Use reset for initialization that has to be done once per
+ # scan.
+ def setup
+ end
+
+ # This is the central method, and commonly the only one a
+ # subclass implements.
+ #
+ # Subclasses must implement this method; it must return +tokens+
+ # and must only use Tokens#<< for storing scanned tokens!
+ def scan_tokens tokens, options
+ raise NotImplementedError,
+ "#{self.class}#scan_tokens not implemented."
+ end
+
+ def reset_instance
+ @tokens.clear unless @options[:keep_tokens]
+ @cached_tokens = nil
+ end
+
+ # Scanner error with additional status information
+ def raise_inspect msg, tokens, state = 'No state given!', ambit = 30
+ raise ScanError, <<-EOE % [
+
+
+***ERROR in %s: %s (after %d tokens)
+
+tokens:
+%s
+
+current line: %d pos = %d
+matched: %p state: %p
+bol? = %p, eos? = %p
+
+surrounding code:
+%p ~~ %p
+
+
+***ERROR***
+
+ EOE
+ File.basename(caller[0]),
+ msg,
+ tokens.size,
+ tokens.last(10).map { |t| t.inspect }.join("\n"),
+ line, pos,
+ matched, state, bol?, eos?,
+ string[pos-ambit,ambit],
+ string[pos,ambit],
+ ]
+ end
+
+ end
+
+ end
+end
+
+class String
+ # I love this hack. It seems to silence all dos/unix/mac newline problems.
+ def to_unix
+ if index ?\r
+ gsub(/\r\n?/, "\n")
+ else
+ self
+ end
+ end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ map :cpp => :c,
+ :plain => :plaintext,
+ :pascal => :delphi,
+ :irb => :ruby,
+ :xml => :html,
+ :xhtml => :nitro_xhtml,
+ :nitro => :nitro_xhtml
+
+ default :plain
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ class C < Scanner
+
+ register_for :c
+
+ include Streamable
+
+ RESERVED_WORDS = [
+ 'asm', 'break', 'case', 'continue', 'default', 'do', 'else',
+ 'for', 'goto', 'if', 'return', 'switch', 'while',
+ 'struct', 'union', 'enum', 'typedef',
+ 'static', 'register', 'auto', 'extern',
+ 'sizeof',
+ 'volatile', 'const', # C89
+ 'inline', 'restrict', # C99
+ ]
+
+ PREDEFINED_TYPES = [
+ 'int', 'long', 'short', 'char', 'void',
+ 'signed', 'unsigned', 'float', 'double',
+ 'bool', 'complex', # C99
+ ]
+
+ PREDEFINED_CONSTANTS = [
+ 'EOF', 'NULL',
+ 'true', 'false', # C99
+ ]
+
+ IDENT_KIND = WordList.new(:ident).
+ add(RESERVED_WORDS, :reserved).
+ add(PREDEFINED_TYPES, :pre_type).
+ add(PREDEFINED_CONSTANTS, :pre_constant)
+
+ ESCAPE = / [rbfnrtv\n\\'"] | x[a-fA-F0-9]{1,2} | [0-7]{1,3} /x
+ UNICODE_ESCAPE = / u[a-fA-F0-9]{4} | U[a-fA-F0-9]{8} /x
+
+ def scan_tokens tokens, options
+
+ state = :initial
+
+ until eos?
+
+ kind = nil
+ match = nil
+
+ case state
+
+ when :initial
+
+ if scan(/ \s+ | \\\n /x)
+ kind = :space
+
+ elsif scan(%r! // [^\n\\]* (?: \\. [^\n\\]* )* | /\* (?: .*? \*/ | .* ) !mx)
+ kind = :comment
+
+ elsif match = scan(/ \# \s* if \s* 0 /x)
+ match << scan_until(/ ^\# (?:elif|else|endif) .*? $ | \z /xm) unless eos?
+ kind = :comment
+
+ elsif scan(/ [-+*\/=<>?:;,!&^|()\[\]{}~%]+ | \.(?!\d) /x)
+ kind = :operator
+
+ elsif match = scan(/ [A-Za-z_][A-Za-z_0-9]* /x)
+ kind = IDENT_KIND[match]
+ if kind == :ident and check(/:(?!:)/)
+ match << scan(/:/)
+ kind = :label
+ end
+
+ elsif match = scan(/L?"/)
+ tokens << [:open, :string]
+ if match[0] == ?L
+ tokens << ['L', :modifier]
+ match = '"'
+ end
+ state = :string
+ kind = :delimiter
+
+ elsif scan(/#\s*(\w*)/)
+ kind = :preprocessor # FIXME multiline preprocs
+ state = :include_expected if self[1] == 'include'
+
+ elsif scan(/ L?' (?: [^\'\n\\] | \\ #{ESCAPE} )? '? /ox)
+ kind = :char
+
+ elsif scan(/0[xX][0-9A-Fa-f]+/)
+ kind = :hex
+
+ elsif scan(/(?:0[0-7]+)(?![89.eEfF])/)
+ kind = :oct
+
+ elsif scan(/(?:\d+)(?![.eEfF])/)
+ kind = :integer
+
+ elsif scan(/\d[fF]?|\d*\.\d+(?:[eE][+-]?\d+)?[fF]?|\d+[eE][+-]?\d+[fF]?/)
+ kind = :float
+
+ else
+ getch
+ kind = :error
+
+ end
+
+ when :string
+ if scan(/[^\\\n"]+/)
+ kind = :content
+ elsif scan(/"/)
+ tokens << ['"', :delimiter]
+ tokens << [:close, :string]
+ state = :initial
+ next
+ elsif scan(/ \\ (?: #{ESCAPE} | #{UNICODE_ESCAPE} ) /mox)
+ kind = :char
+ elsif scan(/ \\ | $ /x)
+ tokens << [:close, :string]
+ kind = :error
+ state = :initial
+ else
+ raise_inspect "else case \" reached; %p not handled." % peek(1), tokens
+ end
+
+ when :include_expected
+ if scan(/[^\n]+/)
+ kind = :include
+ state = :initial
+
+ elsif match = scan(/\s+/)
+ kind = :space
+ state = :initial if match.index ?\n
+
+ else
+ getch
+ kind = :error
+
+ end
+
+ else
+ raise_inspect 'Unknown state', tokens
+
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ tokens << [match, kind]
+
+ end
+
+ if state == :string
+ tokens << [:close, :string]
+ end
+
+ tokens
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ # = Debug Scanner
+ class Debug < Scanner
+
+ include Streamable
+ register_for :debug
+
+ protected
+ def scan_tokens tokens, options
+
+ opened_tokens = []
+
+ until eos?
+
+ kind = nil
+ match = nil
+
+ if scan(/\s+/)
+ tokens << [matched, :space]
+ next
+
+ elsif scan(/ (\w+) \( ( [^\)\\]* ( \\. [^\)\\]* )* ) \) /x)
+ kind = self[1].to_sym
+ match = self[2].gsub(/\\(.)/, '\1')
+
+ elsif scan(/ (\w+) < /x)
+ kind = self[1].to_sym
+ opened_tokens << kind
+ match = :open
+
+ elsif scan(/ > /x)
+ kind = opened_tokens.pop
+ match = :close
+
+ else
+ kind = :error
+ getch
+
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ tokens << [match, kind]
+
+ end
+
+ tokens
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ class Delphi < Scanner
+
+ register_for :delphi
+
+ RESERVED_WORDS = [
+ 'and', 'array', 'as', 'at', 'asm', 'at', 'begin', 'case', 'class',
+ 'const', 'constructor', 'destructor', 'dispinterface', 'div', 'do',
+ 'downto', 'else', 'end', 'except', 'exports', 'file', 'finalization',
+ 'finally', 'for', 'function', 'goto', 'if', 'implementation', 'in',
+ 'inherited', 'initialization', 'inline', 'interface', 'is', 'label',
+ 'library', 'mod', 'nil', 'not', 'object', 'of', 'or', 'out', 'packed',
+ 'procedure', 'program', 'property', 'raise', 'record', 'repeat',
+ 'resourcestring', 'set', 'shl', 'shr', 'string', 'then', 'threadvar',
+ 'to', 'try', 'type', 'unit', 'until', 'uses', 'var', 'while', 'with',
+ 'xor', 'on'
+ ]
+
+ DIRECTIVES = [
+ 'absolute', 'abstract', 'assembler', 'at', 'automated', 'cdecl',
+ 'contains', 'deprecated', 'dispid', 'dynamic', 'export',
+ 'external', 'far', 'forward', 'implements', 'local',
+ 'near', 'nodefault', 'on', 'overload', 'override',
+ 'package', 'pascal', 'platform', 'private', 'protected', 'public',
+ 'published', 'read', 'readonly', 'register', 'reintroduce',
+ 'requires', 'resident', 'safecall', 'stdcall', 'stored', 'varargs',
+ 'virtual', 'write', 'writeonly'
+ ]
+
+ IDENT_KIND = CaseIgnoringWordList.new(:ident, caching=true).
+ add(RESERVED_WORDS, :reserved).
+ add(DIRECTIVES, :directive)
+
+ NAME_FOLLOWS = CaseIgnoringWordList.new(false, caching=true).
+ add(%w(procedure function .))
+
+ private
+ def scan_tokens tokens, options
+
+ state = :initial
+ last_token = ''
+
+ until eos?
+
+ kind = nil
+ match = nil
+
+ if state == :initial
+
+ if scan(/ \s+ /x)
+ tokens << [matched, :space]
+ next
+
+ elsif scan(%r! \{ \$ [^}]* \}? | \(\* \$ (?: .*? \*\) | .* ) !mx)
+ tokens << [matched, :preprocessor]
+ next
+
+ elsif scan(%r! // [^\n]* | \{ [^}]* \}? | \(\* (?: .*? \*\) | .* ) !mx)
+ tokens << [matched, :comment]
+ next
+
+ elsif match = scan(/ <[>=]? | >=? | :=? | [-+=*\/;,@\^|\(\)\[\]] | \.\. /x)
+ kind = :operator
+
+ elsif match = scan(/\./)
+ kind = :operator
+ if last_token == 'end'
+ tokens << [match, kind]
+ next
+ end
+
+ elsif match = scan(/ [A-Za-z_][A-Za-z_0-9]* /x)
+ kind = NAME_FOLLOWS[last_token] ? :ident : IDENT_KIND[match]
+
+ elsif match = scan(/ ' ( [^\n']|'' ) (?:'|$) /x)
+ tokens << [:open, :char]
+ tokens << ["'", :delimiter]
+ tokens << [self[1], :content]
+ tokens << ["'", :delimiter]
+ tokens << [:close, :char]
+ next
+
+ elsif match = scan(/ ' /x)
+ tokens << [:open, :string]
+ state = :string
+ kind = :delimiter
+
+ elsif scan(/ \# (?: \d+ | \$[0-9A-Fa-f]+ ) /x)
+ kind = :char
+
+ elsif scan(/ \$ [0-9A-Fa-f]+ /x)
+ kind = :hex
+
+ elsif scan(/ (?: \d+ ) (?![eE]|\.[^.]) /x)
+ kind = :integer
+
+ elsif scan(/ \d+ (?: \.\d+ (?: [eE][+-]? \d+ )? | [eE][+-]? \d+ ) /x)
+ kind = :float
+
+ else
+ kind = :error
+ getch
+
+ end
+
+ elsif state == :string
+ if scan(/[^\n']+/)
+ kind = :content
+ elsif scan(/''/)
+ kind = :char
+ elsif scan(/'/)
+ tokens << ["'", :delimiter]
+ tokens << [:close, :string]
+ state = :initial
+ next
+ elsif scan(/\n/)
+ tokens << [:close, :string]
+ kind = :error
+ state = :initial
+ else
+ raise "else case \' reached; %p not handled." % peek(1), tokens
+ end
+
+ else
+ raise 'else-case reached', tokens
+
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens, state
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ last_token = match
+ tokens << [match, kind]
+
+ end
+
+ tokens
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ # HTML Scanner
+ #
+ # $Id$
+ class HTML < Scanner
+
+ include Streamable
+ register_for :html
+
+ ATTR_NAME = /[\w.:-]+/
+ ATTR_VALUE_UNQUOTED = ATTR_NAME
+ TAG_END = /\/?>/
+ HEX = /[0-9a-fA-F]/
+ ENTITY = /
+ &
+ (?:
+ \w+
+ |
+ \#
+ (?:
+ \d+
+ |
+ x#{HEX}+
+ )
+ )
+ ;
+ /ox
+
+ PLAIN_STRING_CONTENT = {
+ "'" => /[^&'>\n]+/,
+ '"' => /[^&">\n]+/,
+ }
+
+ def reset
+ super
+ @state = :initial
+ end
+
+ private
+ def setup
+ @state = :initial
+ @plain_string_content = nil
+ end
+
+ def scan_tokens tokens, options
+
+ state = @state
+ plain_string_content = @plain_string_content
+
+ until eos?
+
+ kind = nil
+ match = nil
+
+ if scan(/\s+/m)
+ kind = :space
+
+ else
+
+ case state
+
+ when :initial
+ if scan(/<!--.*?-->/m)
+ kind = :comment
+ elsif scan(/<!DOCTYPE.*?>/m)
+ kind = :preprocessor
+ elsif scan(/<\?xml.*?\?>/m)
+ kind = :preprocessor
+ elsif scan(/<\?.*?\?>|<%.*?%>/m)
+ kind = :comment
+ elsif scan(/<\/[-\w_.:]*>/m)
+ kind = :tag
+ elsif match = scan(/<[-\w_.:]+>?/m)
+ kind = :tag
+ state = :attribute unless match[-1] == ?>
+ elsif scan(/[^<>&]+/)
+ kind = :plain
+ elsif scan(/#{ENTITY}/ox)
+ kind = :entity
+ elsif scan(/[<>&]/)
+ kind = :error
+ else
+ raise_inspect '[BUG] else-case reached with state %p' % [state], tokens
+ end
+
+ when :attribute
+ if scan(/#{TAG_END}/)
+ kind = :tag
+ state = :initial
+ elsif scan(/#{ATTR_NAME}/o)
+ kind = :attribute_name
+ state = :attribute_equal
+ else
+ kind = :error
+ getch
+ end
+
+ when :attribute_equal
+ if scan(/=/)
+ kind = :operator
+ state = :attribute_value
+ elsif scan(/#{ATTR_NAME}/o)
+ kind = :attribute_name
+ elsif scan(/#{TAG_END}/o)
+ kind = :tag
+ state = :initial
+ elsif scan(/./)
+ kind = :error
+ state = :attribute
+ end
+
+ when :attribute_value
+ if scan(/#{ATTR_VALUE_UNQUOTED}/o)
+ kind = :attribute_value
+ state = :attribute
+ elsif match = scan(/["']/)
+ tokens << [:open, :string]
+ state = :attribute_value_string
+ plain_string_content = PLAIN_STRING_CONTENT[match]
+ kind = :delimiter
+ elsif scan(/#{TAG_END}/o)
+ kind = :tag
+ state = :initial
+ else
+ kind = :error
+ getch
+ end
+
+ when :attribute_value_string
+ if scan(plain_string_content)
+ kind = :content
+ elsif scan(/['"]/)
+ tokens << [matched, :delimiter]
+ tokens << [:close, :string]
+ state = :attribute
+ next
+ elsif scan(/#{ENTITY}/ox)
+ kind = :entity
+ elsif scan(/&/)
+ kind = :content
+ elsif scan(/[\n>]/)
+ tokens << [:close, :string]
+ kind = :error
+ state = :initial
+ end
+
+ else
+ raise_inspect 'Unknown state: %p' % [state], tokens
+
+ end
+
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens, state
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ tokens << [match, kind]
+ end
+
+ if options[:keep_state]
+ @state = state
+ @plain_string_content = plain_string_content
+ end
+
+ tokens
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+ module Scanners
+ class Java < Scanner
+
+ register_for :java
+
+ RESERVED_WORDS = %w(abstract assert break case catch class
+ const continue default do else enum extends final finally for
+ goto if implements import instanceof interface native new
+ package private protected public return static strictfp super switch
+ synchronized this throw throws transient try void volatile while)
+
+ PREDEFINED_TYPES = %w(boolean byte char double float int long short)
+
+ PREDEFINED_CONSTANTS = %w(true false null)
+
+ IDENT_KIND = WordList.new(:ident).
+ add(RESERVED_WORDS, :reserved).
+ add(PREDEFINED_TYPES, :pre_type).
+ add(PREDEFINED_CONSTANTS, :pre_constant)
+
+ ESCAPE = / [rbfnrtv\n\\'"] | x[a-fA-F0-9]{1,2} | [0-7]{1,3} /x
+ UNICODE_ESCAPE = / u[a-fA-F0-9]{4} | U[a-fA-F0-9]{8} /x
+
+ def scan_tokens tokens, options
+ state = :initial
+
+ until eos?
+ kind = nil
+ match = nil
+
+ case state
+ when :initial
+
+ if scan(/ \s+ | \\\n /x)
+ kind = :space
+
+ elsif scan(%r! // [^\n\\]* (?: \\. [^\n\\]* )* | /\* (?: .*? \*/ | .* ) !mx)
+ kind = :comment
+
+ elsif match = scan(/ \# \s* if \s* 0 /x)
+ match << scan_until(/ ^\# (?:elif|else|endif) .*? $ | \z /xm) unless eos?
+ kind = :comment
+
+ elsif scan(/ [-+*\/=<>?:;,!&^|()\[\]{}~%]+ | \.(?!\d) /x)
+ kind = :operator
+
+ elsif match = scan(/ [A-Za-z_][A-Za-z_0-9]* /x)
+ kind = IDENT_KIND[match]
+ if kind == :ident and check(/:(?!:)/)
+ match << scan(/:/)
+ kind = :label
+ end
+
+ elsif match = scan(/L?"/)
+ tokens << [:open, :string]
+ if match[0] == ?L
+ tokens << ['L', :modifier]
+ match = '"'
+ end
+ state = :string
+ kind = :delimiter
+
+ elsif scan(%r! \@ .* !x)
+ kind = :preprocessor
+
+ elsif scan(/ L?' (?: [^\'\n\\] | \\ #{ESCAPE} )? '? /ox)
+ kind = :char
+
+ elsif scan(/0[xX][0-9A-Fa-f]+/)
+ kind = :hex
+
+ elsif scan(/(?:0[0-7]+)(?![89.eEfF])/)
+ kind = :oct
+
+ elsif scan(/(?:\d+)(?![.eEfF])/)
+ kind = :integer
+
+ elsif scan(/\d[fF]?|\d*\.\d+(?:[eE][+-]?\d+)?[fF]?|\d+[eE][+-]?\d+[fF]?/)
+ kind = :float
+
+ else
+ getch
+ kind = :error
+
+ end
+
+ when :string
+ if scan(/[^\\\n"]+/)
+ kind = :content
+ elsif scan(/"/)
+ tokens << ['"', :delimiter]
+ tokens << [:close, :string]
+ state = :initial
+ next
+ elsif scan(/ \\ (?: #{ESCAPE} | #{UNICODE_ESCAPE} ) /mox)
+ kind = :char
+ elsif scan(/ \\ | $ /x)
+ tokens << [:close, :string]
+ kind = :error
+ state = :initial
+ else
+ raise_inspect "else case \" reached; %p not handled." % peek(1), tokens
+ end
+
+ else
+ raise_inspect 'Unknown state', tokens
+
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ tokens << [match, kind]
+
+ end
+
+ if state == :string
+ tokens << [:close, :string]
+ end
+
+ tokens
+ end
+ end
+ end
+end
--- /dev/null
+# http://pastie.textmate.org/50774/\r
+module CodeRay module Scanners\r
+ \r
+ class JavaScript < Scanner\r
+\r
+ register_for :javascript\r
+ \r
+ RESERVED_WORDS = [\r
+ 'asm', 'break', 'case', 'continue', 'default', 'do', 'else',\r
+ 'for', 'goto', 'if', 'return', 'switch', 'while',\r
+# 'struct', 'union', 'enum', 'typedef',\r
+# 'static', 'register', 'auto', 'extern',\r
+# 'sizeof',\r
+ 'typeof',\r
+# 'volatile', 'const', # C89\r
+# 'inline', 'restrict', # C99 \r
+ 'var', 'function','try','new','in',\r
+ 'instanceof','throw','catch'\r
+ ]\r
+\r
+ PREDEFINED_CONSTANTS = [\r
+ 'void', 'null', 'this',\r
+ 'true', 'false','undefined',\r
+ ]\r
+\r
+ IDENT_KIND = WordList.new(:ident).\r
+ add(RESERVED_WORDS, :reserved).\r
+ add(PREDEFINED_CONSTANTS, :pre_constant)\r
+\r
+ ESCAPE = / [rbfnrtv\n\\\/'"] | x[a-fA-F0-9]{1,2} | [0-7]{1,3} /x\r
+ UNICODE_ESCAPE = / u[a-fA-F0-9]{4} | U[a-fA-F0-9]{8} /x\r
+\r
+ def scan_tokens tokens, options\r
+\r
+ state = :initial\r
+ string_type = nil\r
+ regexp_allowed = true\r
+\r
+ until eos?\r
+\r
+ kind = :error\r
+ match = nil\r
+\r
+ if state == :initial\r
+ \r
+ if scan(/ \s+ | \\\n /x)\r
+ kind = :space\r
+ \r
+ elsif scan(%r! // [^\n\\]* (?: \\. [^\n\\]* )* | /\* (?: .*? \*/ | .* ) !mx)\r
+ kind = :comment\r
+ regexp_allowed = false\r
+\r
+ elsif match = scan(/ \# \s* if \s* 0 /x)\r
+ match << scan_until(/ ^\# (?:elif|else|endif) .*? $ | \z /xm) unless eos?\r
+ kind = :comment\r
+ regexp_allowed = false\r
+\r
+ elsif regexp_allowed and scan(/\//)\r
+ tokens << [:open, :regexp]\r
+ state = :regex\r
+ kind = :delimiter\r
+ \r
+ elsif scan(/ [-+*\/=<>?:;,!&^|()\[\]{}~%] | \.(?!\d) /x)\r
+ kind = :operator\r
+ regexp_allowed=true\r
+ \r
+ elsif match = scan(/ [$A-Za-z_][A-Za-z_0-9]* /x)\r
+ kind = IDENT_KIND[match]\r
+# if kind == :ident and check(/:(?!:)/)\r
+# match << scan(/:/)\r
+# kind = :label\r
+# end\r
+ regexp_allowed=false\r
+ \r
+ elsif match = scan(/["']/)\r
+ tokens << [:open, :string]\r
+ string_type = matched\r
+ state = :string\r
+ kind = :delimiter\r
+ \r
+# elsif scan(/#\s*(\w*)/)\r
+# kind = :preprocessor # FIXME multiline preprocs\r
+# state = :include_expected if self[1] == 'include'\r
+# \r
+# elsif scan(/ L?' (?: [^\'\n\\] | \\ #{ESCAPE} )? '? /ox)\r
+# kind = :char\r
+ \r
+ elsif scan(/0[xX][0-9A-Fa-f]+/)\r
+ kind = :hex\r
+ regexp_allowed=false\r
+ \r
+ elsif scan(/(?:0[0-7]+)(?![89.eEfF])/)\r
+ kind = :oct\r
+ regexp_allowed=false\r
+ \r
+ elsif scan(/(?:\d+)(?![.eEfF])/)\r
+ kind = :integer\r
+ regexp_allowed=false\r
+ \r
+ elsif scan(/\d[fF]?|\d*\.\d+(?:[eE][+-]?\d+)?[fF]?|\d+[eE][+-]?\d+[fF]?/)\r
+ kind = :float\r
+ regexp_allowed=false\r
+\r
+ else\r
+ getch\r
+ end\r
+ \r
+ elsif state == :regex\r
+ if scan(/[^\\\/]+/)\r
+ kind = :content\r
+ elsif scan(/\\\/|\\\\/)\r
+ kind = :content\r
+ elsif scan(/\//)\r
+ tokens << [matched, :delimiter]\r
+ tokens << [:close, :regexp]\r
+ state = :initial\r
+ next\r
+ else\r
+ getch\r
+ kind = :content\r
+ end\r
+ \r
+ elsif state == :string\r
+ if scan(/[^\\"']+/)\r
+ kind = :content\r
+ elsif scan(/["']/)\r
+ if string_type==matched\r
+ tokens << [matched, :delimiter]\r
+ tokens << [:close, :string]\r
+ state = :initial\r
+ string_type=nil\r
+ next\r
+ else\r
+ kind = :content\r
+ end\r
+ elsif scan(/ \\ (?: #{ESCAPE} | #{UNICODE_ESCAPE} ) /mox)\r
+ kind = :char\r
+ elsif scan(/ \\ | $ /x)\r
+ kind = :error\r
+ state = :initial\r
+ else\r
+ raise "else case \" reached; %p not handled." % peek(1), tokens\r
+ end\r
+ \r
+# elsif state == :include_expected\r
+# if scan(/<[^>\n]+>?|"[^"\n\\]*(?:\\.[^"\n\\]*)*"?/)\r
+# kind = :include\r
+# state = :initial\r
+# \r
+# elsif match = scan(/\s+/)\r
+# kind = :space\r
+# state = :initial if match.index ?\n\r
+# \r
+# else\r
+# getch\r
+# \r
+# end\r
+# \r
+ else\r
+ raise 'else-case reached', tokens\r
+ \r
+ end\r
+ \r
+ match ||= matched\r
+# raise [match, kind], tokens if kind == :error\r
+ \r
+ tokens << [match, kind]\r
+ \r
+ end\r
+ tokens\r
+ \r
+ end\r
+\r
+ end\r
+\r
+end end
\ No newline at end of file
--- /dev/null
+module CodeRay
+module Scanners
+
+ load :html
+ load :ruby
+
+ # Nitro XHTML Scanner
+ #
+ # $Id$
+ class NitroXHTML < Scanner
+
+ include Streamable
+ register_for :nitro_xhtml
+
+ NITRO_RUBY_BLOCK = /
+ <\?r
+ (?>
+ [^\?]*
+ (?> \?(?!>) [^\?]* )*
+ )
+ (?: \?> )?
+ |
+ <ruby>
+ (?>
+ [^<]*
+ (?> <(?!\/ruby>) [^<]* )*
+ )
+ (?: <\/ruby> )?
+ |
+ <%
+ (?>
+ [^%]*
+ (?> %(?!>) [^%]* )*
+ )
+ (?: %> )?
+ /mx
+
+ NITRO_VALUE_BLOCK = /
+ \#
+ (?:
+ \{
+ [^{}]*
+ (?>
+ \{ [^}]* \}
+ (?> [^{}]* )
+ )*
+ \}?
+ | \| [^|]* \|?
+ | \( [^)]* \)?
+ | \[ [^\]]* \]?
+ | \\ [^\\]* \\?
+ )
+ /x
+
+ NITRO_ENTITY = /
+ % (?: \#\d+ | \w+ ) ;
+ /
+
+ START_OF_RUBY = /
+ (?=[<\#%])
+ < (?: \?r | % | ruby> )
+ | \# [{(|]
+ | % (?: \#\d+ | \w+ ) ;
+ /x
+
+ CLOSING_PAREN = Hash.new do |h, p|
+ h[p] = p
+ end.update( {
+ '(' => ')',
+ '[' => ']',
+ '{' => '}',
+ } )
+
+ private
+
+ def setup
+ @ruby_scanner = CodeRay.scanner :ruby, :tokens => @tokens, :keep_tokens => true
+ @html_scanner = CodeRay.scanner :html, :tokens => @tokens, :keep_tokens => true, :keep_state => true
+ end
+
+ def reset_instance
+ super
+ @html_scanner.reset
+ end
+
+ def scan_tokens tokens, options
+
+ until eos?
+
+ if (match = scan_until(/(?=#{START_OF_RUBY})/o) || scan_until(/\z/)) and not match.empty?
+ @html_scanner.tokenize match
+
+ elsif match = scan(/#{NITRO_VALUE_BLOCK}/o)
+ start_tag = match[0,2]
+ delimiter = CLOSING_PAREN[start_tag[1,1]]
+ end_tag = match[-1,1] == delimiter ? delimiter : ''
+ tokens << [:open, :inline]
+ tokens << [start_tag, :inline_delimiter]
+ code = match[start_tag.size .. -1 - end_tag.size]
+ @ruby_scanner.tokenize code
+ tokens << [end_tag, :inline_delimiter] unless end_tag.empty?
+ tokens << [:close, :inline]
+
+ elsif match = scan(/#{NITRO_RUBY_BLOCK}/o)
+ start_tag = '<?r'
+ end_tag = match[-2,2] == '?>' ? '?>' : ''
+ tokens << [:open, :inline]
+ tokens << [start_tag, :inline_delimiter]
+ code = match[start_tag.size .. -(end_tag.size)-1]
+ @ruby_scanner.tokenize code
+ tokens << [end_tag, :inline_delimiter] unless end_tag.empty?
+ tokens << [:close, :inline]
+
+ elsif entity = scan(/#{NITRO_ENTITY}/o)
+ tokens << [entity, :entity]
+
+ elsif scan(/%/)
+ tokens << [matched, :error]
+
+ else
+ raise_inspect 'else-case reached!', tokens
+ end
+
+ end
+
+ tokens
+
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay module Scanners
+
+ class PHP < Scanner
+
+ register_for :php
+
+ RESERVED_WORDS = [
+ 'and', 'or', 'xor', '__FILE__', 'exception', '__LINE__', 'array', 'as', 'break', 'case',
+ 'class', 'const', 'continue', 'declare', 'default',
+ 'die', 'do', 'echo', 'else', 'elseif',
+ 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif',
+ 'endswitch', 'endwhile', 'eval', 'exit', 'extends',
+ 'for', 'foreach', 'function', 'global', 'if',
+ 'include', 'include_once', 'isset', 'list', 'new',
+ 'print', 'require', 'require_once', 'return', 'static',
+ 'switch', 'unset', 'use', 'var', 'while',
+ '__FUNCTION__', '__CLASS__', '__METHOD__', 'final', 'php_user_filter',
+ 'interface', 'implements', 'extends', 'public', 'private',
+ 'protected', 'abstract', 'clone', 'try', 'catch',
+ 'throw', 'cfunction', 'old_function'
+ ]
+
+ PREDEFINED_CONSTANTS = [
+ 'null', '$this', 'true', 'false'
+ ]
+
+ IDENT_KIND = WordList.new(:ident).
+ add(RESERVED_WORDS, :reserved).
+ add(PREDEFINED_CONSTANTS, :pre_constant)
+
+ ESCAPE = / [\$\wrbfnrtv\n\\\/'"] | x[a-fA-F0-9]{1,2} | [0-7]{1,3} /x
+ UNICODE_ESCAPE = / u[a-fA-F0-9]{4} | U[a-fA-F0-9]{8} /x
+
+ def scan_tokens tokens, options
+
+ state = :waiting_php
+ string_type = nil
+ regexp_allowed = true
+
+ until eos?
+
+ kind = :error
+ match = nil
+
+ if state == :initial
+
+ if scan(/ \s+ | \\\n /x)
+ kind = :space
+
+ elsif scan(/\?>/)
+ kind = :char
+ state = :waiting_php
+
+ elsif scan(%r{ (//|\#) [^\n\\]* (?: \\. [^\n\\]* )* | /\* (?: .*? \*/ | .* ) }mx)
+ kind = :comment
+ regexp_allowed = false
+
+ elsif match = scan(/ \# \s* if \s* 0 /x)
+ match << scan_until(/ ^\# (?:elif|else|endif) .*? $ | \z /xm) unless eos?
+ kind = :comment
+ regexp_allowed = false
+
+ elsif regexp_allowed and scan(/\//)
+ tokens << [:open, :regexp]
+ state = :regex
+ kind = :delimiter
+
+ elsif scan(/ [-+*\/=<>?:;,!&^|()\[\]{}~%] | \.(?!\d) /x)
+ kind = :operator
+ regexp_allowed=true
+
+ elsif match = scan(/ [$@A-Za-z_][A-Za-z_0-9]* /x)
+ kind = IDENT_KIND[match]
+ regexp_allowed=false
+
+ elsif match = scan(/["']/)
+ tokens << [:open, :string]
+ string_type = matched
+ state = :string
+ kind = :delimiter
+
+ elsif scan(/0[xX][0-9A-Fa-f]+/)
+ kind = :hex
+ regexp_allowed=false
+
+ elsif scan(/(?:0[0-7]+)(?![89.eEfF])/)
+ kind = :oct
+ regexp_allowed=false
+
+ elsif scan(/(?:\d+)(?![.eEfF])/)
+ kind = :integer
+ regexp_allowed=false
+
+ elsif scan(/\d[fF]?|\d*\.\d+(?:[eE][+-]?\d+)?[fF]?|\d+[eE][+-]?\d+[fF]?/)
+ kind = :float
+ regexp_allowed=false
+
+ else
+ getch
+ end
+
+ elsif state == :regex
+ if scan(/[^\\\/]+/)
+ kind = :content
+ elsif scan(/\\\/|\\/)
+ kind = :content
+ elsif scan(/\//)
+ tokens << [matched, :delimiter]
+ tokens << [:close, :regexp]
+ state = :initial
+ next
+ else
+ getch
+ kind = :content
+ end
+
+ elsif state == :string
+ if scan(/[^\\"']+/)
+ kind = :content
+ elsif scan(/["']/)
+ if string_type==matched
+ tokens << [matched, :delimiter]
+ tokens << [:close, :string]
+ state = :initial
+ string_type=nil
+ next
+ else
+ kind = :content
+ end
+ elsif scan(/ \\ (?: \S ) /mox)
+ kind = :char
+ elsif scan(/ \\ | $ /x)
+ kind = :error
+ state = :initial
+ else
+ raise "else case \" reached; %p not handled." % peek(1), tokens
+ end
+
+ elsif state == :waiting_php
+ if scan(/<\?php/m)
+ kind = :char
+ state = :initial
+ elsif scan(/[^<]+/)
+ kind = :comment
+ else
+ kind = :comment
+ getch
+ end
+ else
+ raise 'else-case reached', tokens
+
+ end
+
+ match ||= matched
+
+ tokens << [match, kind]
+
+ end
+ tokens
+
+ end
+
+ end
+
+end end
\ No newline at end of file
--- /dev/null
+module CodeRay
+module Scanners
+
+ class Plaintext < Scanner
+
+ register_for :plaintext, :plain
+
+ include Streamable
+
+ def scan_tokens tokens, options
+ text = (scan_until(/\z/) || '')
+ tokens << [text, :plain]
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ load :html
+ load :ruby
+
+ # RHTML Scanner
+ #
+ # $Id$
+ class RHTML < Scanner
+
+ include Streamable
+ register_for :rhtml
+
+ ERB_RUBY_BLOCK = /
+ <%(?!%)[=-]?
+ (?>
+ [^\-%]* # normal*
+ (?> # special
+ (?: %(?!>) | -(?!%>) )
+ [^\-%]* # normal*
+ )*
+ )
+ (?: -?%> )?
+ /x
+
+ START_OF_ERB = /
+ <%(?!%)
+ /x
+
+ private
+
+ def setup
+ @ruby_scanner = CodeRay.scanner :ruby, :tokens => @tokens, :keep_tokens => true
+ @html_scanner = CodeRay.scanner :html, :tokens => @tokens, :keep_tokens => true, :keep_state => true
+ end
+
+ def reset_instance
+ super
+ @html_scanner.reset
+ end
+
+ def scan_tokens tokens, options
+
+ until eos?
+
+ if (match = scan_until(/(?=#{START_OF_ERB})/o) || scan_until(/\z/)) and not match.empty?
+ @html_scanner.tokenize match
+
+ elsif match = scan(/#{ERB_RUBY_BLOCK}/o)
+ start_tag = match[/\A<%[-=]?/]
+ end_tag = match[/-?%?>?\z/]
+ tokens << [:open, :inline]
+ tokens << [start_tag, :inline_delimiter]
+ code = match[start_tag.size .. -1 - end_tag.size]
+ @ruby_scanner.tokenize code
+ tokens << [end_tag, :inline_delimiter] unless end_tag.empty?
+ tokens << [:close, :inline]
+
+ else
+ raise_inspect 'else-case reached!', tokens
+ end
+
+ end
+
+ tokens
+
+ end
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Scanners
+
+ # This scanner is really complex, since Ruby _is_ a complex language!
+ #
+ # It tries to highlight 100% of all common code,
+ # and 90% of strange codes.
+ #
+ # It is optimized for HTML highlighting, and is not very useful for
+ # parsing or pretty printing.
+ #
+ # For now, I think it's better than the scanners in VIM or Syntax, or
+ # any highlighter I was able to find, except Caleb's RubyLexer.
+ #
+ # I hope it's also better than the rdoc/irb lexer.
+ class Ruby < Scanner
+
+ include Streamable
+
+ register_for :ruby
+ file_extension 'rb'
+
+ helper :patterns
+
+ private
+ def scan_tokens tokens, options
+ last_token_dot = false
+ value_expected = true
+ heredocs = nil
+ last_state = nil
+ state = :initial
+ depth = nil
+ inline_block_stack = []
+
+ patterns = Patterns # avoid constant lookup
+
+ until eos?
+ match = nil
+ kind = nil
+
+ if state.instance_of? patterns::StringState
+# {{{
+ match = scan_until(state.pattern) || scan_until(/\z/)
+ tokens << [match, :content] unless match.empty?
+ break if eos?
+
+ if state.heredoc and self[1] # end of heredoc
+ match = getch.to_s
+ match << scan_until(/$/) unless eos?
+ tokens << [match, :delimiter]
+ tokens << [:close, state.type]
+ state = state.next_state
+ next
+ end
+
+ case match = getch
+
+ when state.delim
+ if state.paren
+ state.paren_depth -= 1
+ if state.paren_depth > 0
+ tokens << [match, :nesting_delimiter]
+ next
+ end
+ end
+ tokens << [match, :delimiter]
+ if state.type == :regexp and not eos?
+ modifiers = scan(/#{patterns::REGEXP_MODIFIERS}/ox)
+ tokens << [modifiers, :modifier] unless modifiers.empty?
+ end
+ tokens << [:close, state.type]
+ value_expected = false
+ state = state.next_state
+
+ when '\\'
+ if state.interpreted
+ if esc = scan(/ #{patterns::ESCAPE} /ox)
+ tokens << [match + esc, :char]
+ else
+ tokens << [match, :error]
+ end
+ else
+ case m = getch
+ when state.delim, '\\'
+ tokens << [match + m, :char]
+ when nil
+ tokens << [match, :error]
+ else
+ tokens << [match + m, :content]
+ end
+ end
+
+ when '#'
+ case peek(1)
+ when '{'
+ inline_block_stack << [state, depth, heredocs]
+ value_expected = true
+ state = :initial
+ depth = 1
+ tokens << [:open, :inline]
+ tokens << [match + getch, :inline_delimiter]
+ when '$', '@'
+ tokens << [match, :escape]
+ last_state = state # scan one token as normal code, then return here
+ state = :initial
+ else
+ raise_inspect 'else-case # reached; #%p not handled' % peek(1), tokens
+ end
+
+ when state.paren
+ state.paren_depth += 1
+ tokens << [match, :nesting_delimiter]
+
+ when /#{patterns::REGEXP_SYMBOLS}/ox
+ tokens << [match, :function]
+
+ else
+ raise_inspect 'else-case " reached; %p not handled, state = %p' % [match, state], tokens
+
+ end
+ next
+# }}}
+ else
+# {{{
+ if match = scan(/[ \t\f]+/)
+ kind = :space
+ match << scan(/\s*/) unless eos? or heredocs
+ tokens << [match, kind]
+ next
+
+ elsif match = scan(/\\?\n/)
+ kind = :space
+ if match == "\n"
+ value_expected = true # FIXME not quite true
+ state = :initial if state == :undef_comma_expected
+ end
+ if heredocs
+ unscan # heredoc scanning needs \n at start
+ state = heredocs.shift
+ tokens << [:open, state.type]
+ heredocs = nil if heredocs.empty?
+ next
+ else
+ match << scan(/\s*/) unless eos?
+ end
+ tokens << [match, kind]
+ next
+
+ elsif match = scan(/\#.*/) or
+ ( bol? and match = scan(/#{patterns::RUBYDOC_OR_DATA}/o) )
+ kind = :comment
+ value_expected = true
+ tokens << [match, kind]
+ next
+
+ elsif state == :initial
+
+ # IDENTS #
+ if match = scan(/#{patterns::METHOD_NAME}/o)
+ if last_token_dot
+ kind = if match[/^[A-Z]/] and not match?(/\(/) then :constant else :ident end
+ else
+ kind = patterns::IDENT_KIND[match]
+ if kind == :ident and match[/^[A-Z]/] and not match[/[!?]$/] and not match?(/\(/)
+ kind = :constant
+ elsif kind == :reserved
+ state = patterns::DEF_NEW_STATE[match]
+ end
+ end
+ ## experimental!
+ value_expected = :set if
+ patterns::REGEXP_ALLOWED[match] or check(/#{patterns::VALUE_FOLLOWS}/o)
+
+ elsif last_token_dot and match = scan(/#{patterns::METHOD_NAME_OPERATOR}/o)
+ kind = :ident
+ value_expected = :set if check(/#{patterns::VALUE_FOLLOWS}/o)
+
+ # OPERATORS #
+ elsif not last_token_dot and match = scan(/ \.\.\.? | (?:\.|::)() | [,\(\)\[\]\{\}] | ==?=? /x)
+ if match !~ / [.\)\]\}] /x or match =~ /\.\.\.?/
+ value_expected = :set
+ end
+ last_token_dot = :set if self[1]
+ kind = :operator
+ unless inline_block_stack.empty?
+ case match
+ when '{'
+ depth += 1
+ when '}'
+ depth -= 1
+ if depth == 0 # closing brace of inline block reached
+ state, depth, heredocs = inline_block_stack.pop
+ tokens << [match, :inline_delimiter]
+ kind = :inline
+ match = :close
+ end
+ end
+ end
+
+ elsif match = scan(/ ['"] /mx)
+ tokens << [:open, :string]
+ kind = :delimiter
+ state = patterns::StringState.new :string, match == '"', match # important for streaming
+
+ elsif match = scan(/#{patterns::INSTANCE_VARIABLE}/o)
+ kind = :instance_variable
+
+ elsif value_expected and match = scan(/\//)
+ tokens << [:open, :regexp]
+ kind = :delimiter
+ interpreted = true
+ state = patterns::StringState.new :regexp, interpreted, match
+
+ elsif match = scan(/#{patterns::NUMERIC}/o)
+ kind = if self[1] then :float else :integer end
+
+ elsif match = scan(/#{patterns::SYMBOL}/o)
+ case delim = match[1]
+ when ?', ?"
+ tokens << [:open, :symbol]
+ tokens << [':', :symbol]
+ match = delim.chr
+ kind = :delimiter
+ state = patterns::StringState.new :symbol, delim == ?", match
+ else
+ kind = :symbol
+ end
+
+ elsif match = scan(/ [-+!~^]=? | [*|&]{1,2}=? | >>? /x)
+ value_expected = :set
+ kind = :operator
+
+ elsif value_expected and match = scan(/#{patterns::HEREDOC_OPEN}/o)
+ indented = self[1] == '-'
+ quote = self[3]
+ delim = self[quote ? 4 : 2]
+ kind = patterns::QUOTE_TO_TYPE[quote]
+ tokens << [:open, kind]
+ tokens << [match, :delimiter]
+ match = :close
+ heredoc = patterns::StringState.new kind, quote != '\'', delim, (indented ? :indented : :linestart )
+ heredocs ||= [] # create heredocs if empty
+ heredocs << heredoc
+
+ elsif value_expected and match = scan(/#{patterns::FANCY_START_CORRECT}/o)
+ kind, interpreted = *patterns::FancyStringType.fetch(self[1]) do
+ raise_inspect 'Unknown fancy string: %%%p' % k, tokens
+ end
+ tokens << [:open, kind]
+ state = patterns::StringState.new kind, interpreted, self[2]
+ kind = :delimiter
+
+ elsif value_expected and match = scan(/#{patterns::CHARACTER}/o)
+ kind = :integer
+
+ elsif match = scan(/ [\/%]=? | <(?:<|=>?)? | [?:;] /x)
+ value_expected = :set
+ kind = :operator
+
+ elsif match = scan(/`/)
+ if last_token_dot
+ kind = :operator
+ else
+ tokens << [:open, :shell]
+ kind = :delimiter
+ state = patterns::StringState.new :shell, true, match
+ end
+
+ elsif match = scan(/#{patterns::GLOBAL_VARIABLE}/o)
+ kind = :global_variable
+
+ elsif match = scan(/#{patterns::CLASS_VARIABLE}/o)
+ kind = :class_variable
+
+ else
+ kind = :error
+ match = getch
+
+ end
+
+ elsif state == :def_expected
+ state = :initial
+ if match = scan(/(?>#{patterns::METHOD_NAME_EX})(?!\.|::)/o)
+ kind = :method
+ else
+ next
+ end
+
+ elsif state == :undef_expected
+ state = :undef_comma_expected
+ if match = scan(/#{patterns::METHOD_NAME_EX}/o)
+ kind = :method
+ elsif match = scan(/#{patterns::SYMBOL}/o)
+ case delim = match[1]
+ when ?', ?"
+ tokens << [:open, :symbol]
+ tokens << [':', :symbol]
+ match = delim.chr
+ kind = :delimiter
+ state = patterns::StringState.new :symbol, delim == ?", match
+ state.next_state = :undef_comma_expected
+ else
+ kind = :symbol
+ end
+ else
+ state = :initial
+ next
+ end
+
+ elsif state == :undef_comma_expected
+ if match = scan(/,/)
+ kind = :operator
+ state = :undef_expected
+ else
+ state = :initial
+ next
+ end
+
+ elsif state == :module_expected
+ if match = scan(/<</)
+ kind = :operator
+ else
+ state = :initial
+ if match = scan(/ (?:#{patterns::IDENT}::)* #{patterns::IDENT} /ox)
+ kind = :class
+ else
+ next
+ end
+ end
+
+ end
+# }}}
+
+ value_expected = value_expected == :set
+ last_token_dot = last_token_dot == :set
+
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens, state
+ end
+ raise_inspect 'Empty token', tokens unless match
+
+ tokens << [match, kind]
+
+ if last_state
+ state = last_state
+ last_state = nil
+ end
+ end
+ end
+
+ inline_block_stack << [state] if state.is_a? patterns::StringState
+ until inline_block_stack.empty?
+ this_block = inline_block_stack.pop
+ tokens << [:close, :inline] if this_block.size > 1
+ state = this_block.first
+ tokens << [:close, state.type]
+ end
+
+ tokens
+ end
+
+ end
+
+end
+end
+
+# vim:fdm=marker
--- /dev/null
+module CodeRay
+module Scanners
+
+ module Ruby::Patterns # :nodoc:
+
+ RESERVED_WORDS = %w[
+ and def end in or unless begin
+ defined? ensure module redo super until
+ BEGIN break do next rescue then
+ when END case else for retry
+ while alias class elsif if not return
+ undef yield
+ ]
+
+ DEF_KEYWORDS = %w[ def ]
+ UNDEF_KEYWORDS = %w[ undef ]
+ MODULE_KEYWORDS = %w[class module]
+ DEF_NEW_STATE = WordList.new(:initial).
+ add(DEF_KEYWORDS, :def_expected).
+ add(UNDEF_KEYWORDS, :undef_expected).
+ add(MODULE_KEYWORDS, :module_expected)
+
+ IDENTS_ALLOWING_REGEXP = %w[
+ and or not while until unless if then elsif when sub sub! gsub gsub!
+ scan slice slice! split
+ ]
+ REGEXP_ALLOWED = WordList.new(false).
+ add(IDENTS_ALLOWING_REGEXP, :set)
+
+ PREDEFINED_CONSTANTS = %w[
+ nil true false self
+ DATA ARGV ARGF __FILE__ __LINE__
+ ]
+
+ IDENT_KIND = WordList.new(:ident).
+ add(RESERVED_WORDS, :reserved).
+ add(PREDEFINED_CONSTANTS, :pre_constant)
+
+ IDENT = /[a-z_][\w_]*/i
+
+ METHOD_NAME = / #{IDENT} [?!]? /ox
+ METHOD_NAME_OPERATOR = /
+ \*\*? # multiplication and power
+ | [-+]@? # plus, minus
+ | [\/%&|^`~] # division, modulo or format strings, &and, |or, ^xor, `system`, tilde
+ | \[\]=? # array getter and setter
+ | << | >> # append or shift left, shift right
+ | <=?>? | >=? # comparison, rocket operator
+ | ===? # simple equality and case equality
+ /ox
+ METHOD_NAME_EX = / #{IDENT} (?:[?!]|=(?!>))? | #{METHOD_NAME_OPERATOR} /ox
+ INSTANCE_VARIABLE = / @ #{IDENT} /ox
+ CLASS_VARIABLE = / @@ #{IDENT} /ox
+ OBJECT_VARIABLE = / @@? #{IDENT} /ox
+ GLOBAL_VARIABLE = / \$ (?: #{IDENT} | [1-9]\d* | 0\w* | [~&+`'=\/,;_.<>!@$?*":\\] | -[a-zA-Z_0-9] ) /ox
+ PREFIX_VARIABLE = / #{GLOBAL_VARIABLE} |#{OBJECT_VARIABLE} /ox
+ VARIABLE = / @?@? #{IDENT} | #{GLOBAL_VARIABLE} /ox
+
+ QUOTE_TO_TYPE = {
+ '`' => :shell,
+ '/'=> :regexp,
+ }
+ QUOTE_TO_TYPE.default = :string
+
+ REGEXP_MODIFIERS = /[mixounse]*/
+ REGEXP_SYMBOLS = /[|?*+?(){}\[\].^$]/
+
+ DECIMAL = /\d+(?:_\d+)*/
+ OCTAL = /0_?[0-7]+(?:_[0-7]+)*/
+ HEXADECIMAL = /0x[0-9A-Fa-f]+(?:_[0-9A-Fa-f]+)*/
+ BINARY = /0b[01]+(?:_[01]+)*/
+
+ EXPONENT = / [eE] [+-]? #{DECIMAL} /ox
+ FLOAT_SUFFIX = / #{EXPONENT} | \. #{DECIMAL} #{EXPONENT}? /ox
+ FLOAT_OR_INT = / #{DECIMAL} (?: #{FLOAT_SUFFIX} () )? /ox
+ NUMERIC = / [-+]? (?: (?=0) (?: #{OCTAL} | #{HEXADECIMAL} | #{BINARY} ) | #{FLOAT_OR_INT} ) /ox
+
+ SYMBOL = /
+ :
+ (?:
+ #{METHOD_NAME_EX}
+ | #{PREFIX_VARIABLE}
+ | ['"]
+ )
+ /ox
+
+ # TODO investigste \M, \c and \C escape sequences
+ # (?: M-\\C-|C-\\M-|M-\\c|c\\M-|c|C-|M-)? (?: \\ (?: [0-7]{3} | x[0-9A-Fa-f]{2} | . ) )
+ # assert_equal(225, ?\M-a)
+ # assert_equal(129, ?\M-\C-a)
+ ESCAPE = /
+ [abefnrstv]
+ | M-\\C-|C-\\M-|M-\\c|c\\M-|c|C-|M-
+ | [0-7]{1,3}
+ | x[0-9A-Fa-f]{1,2}
+ | .
+ /mx
+
+ CHARACTER = /
+ \?
+ (?:
+ [^\s\\]
+ | \\ #{ESCAPE}
+ )
+ /mx
+
+ # NOTE: This is not completely correct, but
+ # nobody needs heredoc delimiters ending with \n.
+ HEREDOC_OPEN = /
+ << (-)? # $1 = float
+ (?:
+ ( [A-Za-z_0-9]+ ) # $2 = delim
+ |
+ ( ["'`\/] ) # $3 = quote, type
+ ( [^\n]*? ) \3 # $4 = delim
+ )
+ /mx
+
+ RUBYDOC = /
+ =begin (?!\S)
+ .*?
+ (?: \Z | ^=end (?!\S) [^\n]* )
+ /mx
+
+ DATA = /
+ __END__$
+ .*?
+ (?: \Z | (?=^\#CODE) )
+ /mx
+
+ # Checks for a valid value to follow. This enables
+ # fancy_allowed in method calls.
+ VALUE_FOLLOWS = /
+ \s+
+ (?:
+ [%\/][^\s=]
+ |
+ <<-?\S
+ |
+ #{CHARACTER}
+ )
+ /x
+
+ RUBYDOC_OR_DATA = / #{RUBYDOC} | #{DATA} /xo
+
+ RDOC_DATA_START = / ^=begin (?!\S) | ^__END__$ /x
+
+ # FIXME: \s and = are only a workaround, they are still allowed
+ # as delimiters.
+ FANCY_START_SAVE = / % ( [qQwWxsr] | (?![a-zA-Z0-9\s=]) ) ([^a-zA-Z0-9]) /mx
+ FANCY_START_CORRECT = / % ( [qQwWxsr] | (?![a-zA-Z0-9]) ) ([^a-zA-Z0-9]) /mx
+
+ FancyStringType = {
+ 'q' => [:string, false],
+ 'Q' => [:string, true],
+ 'r' => [:regexp, true],
+ 's' => [:symbol, false],
+ 'x' => [:shell, true]
+ }
+ FancyStringType['w'] = FancyStringType['q']
+ FancyStringType['W'] = FancyStringType[''] = FancyStringType['Q']
+
+ class StringState < Struct.new :type, :interpreted, :delim, :heredoc,
+ :paren, :paren_depth, :pattern, :next_state
+
+ CLOSING_PAREN = Hash[ *%w[
+ ( )
+ [ ]
+ < >
+ { }
+ ] ]
+
+ CLOSING_PAREN.values.each { |o| o.freeze } # debug, if I try to change it with <<
+ OPENING_PAREN = CLOSING_PAREN.invert
+
+ STRING_PATTERN = Hash.new { |h, k|
+ delim, interpreted = *k
+ delim_pattern = Regexp.escape(delim.dup)
+ if closing_paren = CLOSING_PAREN[delim]
+ delim_pattern << Regexp.escape(closing_paren)
+ end
+
+
+ special_escapes =
+ case interpreted
+ when :regexp_symbols
+ '| ' + REGEXP_SYMBOLS.source
+ when :words
+ '| \s'
+ end
+
+ h[k] =
+ if interpreted and not delim == '#'
+ / (?= [#{delim_pattern}\\] | \# [{$@] #{special_escapes} ) /mx
+ else
+ / (?= [#{delim_pattern}\\] #{special_escapes} ) /mx
+ end
+ }
+
+ HEREDOC_PATTERN = Hash.new { |h, k|
+ delim, interpreted, indented = *k
+ delim_pattern = Regexp.escape(delim.dup)
+ delim_pattern = / \n #{ '(?>[\ \t]*)' if indented } #{ Regexp.new delim_pattern } $ /x
+ h[k] =
+ if interpreted
+ / (?= #{delim_pattern}() | \\ | \# [{$@] ) /mx # $1 set == end of heredoc
+ else
+ / (?= #{delim_pattern}() | \\ ) /mx
+ end
+ }
+
+ def initialize kind, interpreted, delim, heredoc = false
+ if heredoc
+ pattern = HEREDOC_PATTERN[ [delim, interpreted, heredoc == :indented] ]
+ delim = nil
+ else
+ pattern = STRING_PATTERN[ [delim, interpreted] ]
+ if paren = CLOSING_PAREN[delim]
+ delim, paren = paren, delim
+ paren_depth = 1
+ end
+ end
+ super kind, interpreted, delim, heredoc, paren, paren_depth, pattern, :initial
+ end
+ end unless defined? StringState
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+ module Scanners
+
+ # Scheme scanner for CodeRay (by closure).
+ # Thanks to murphy for putting CodeRay into public.
+ class Scheme < Scanner
+
+ register_for :scheme
+ file_extension :scm
+
+ CORE_FORMS = %w[
+ lambda let let* letrec syntax-case define-syntax let-syntax
+ letrec-syntax begin define quote if or and cond case do delay
+ quasiquote set! cons force call-with-current-continuation call/cc
+ ]
+
+ IDENT_KIND = CaseIgnoringWordList.new(:ident).
+ add(CORE_FORMS, :reserved)
+
+ #IDENTIFIER_INITIAL = /[a-z!@\$%&\*\/\:<=>\?~_\^]/i
+ #IDENTIFIER_SUBSEQUENT = /#{IDENTIFIER_INITIAL}|\d|\.|\+|-/
+ #IDENTIFIER = /#{IDENTIFIER_INITIAL}#{IDENTIFIER_SUBSEQUENT}*|\+|-|\.{3}/
+ IDENTIFIER = /[a-zA-Z!@$%&*\/:<=>?~_^][\w!@$%&*\/:<=>?~^.+\-]*|[+-]|\.\.\./
+ DIGIT = /\d/
+ DIGIT10 = DIGIT
+ DIGIT16 = /[0-9a-f]/i
+ DIGIT8 = /[0-7]/
+ DIGIT2 = /[01]/
+ RADIX16 = /\#x/i
+ RADIX8 = /\#o/i
+ RADIX2 = /\#b/i
+ RADIX10 = /\#d/i
+ EXACTNESS = /#i|#e/i
+ SIGN = /[\+-]?/
+ EXP_MARK = /[esfdl]/i
+ EXP = /#{EXP_MARK}#{SIGN}#{DIGIT}+/
+ SUFFIX = /#{EXP}?/
+ PREFIX10 = /#{RADIX10}?#{EXACTNESS}?|#{EXACTNESS}?#{RADIX10}?/
+ PREFIX16 = /#{RADIX16}#{EXACTNESS}?|#{EXACTNESS}?#{RADIX16}/
+ PREFIX8 = /#{RADIX8}#{EXACTNESS}?|#{EXACTNESS}?#{RADIX8}/
+ PREFIX2 = /#{RADIX2}#{EXACTNESS}?|#{EXACTNESS}?#{RADIX2}/
+ UINT10 = /#{DIGIT10}+#*/
+ UINT16 = /#{DIGIT16}+#*/
+ UINT8 = /#{DIGIT8}+#*/
+ UINT2 = /#{DIGIT2}+#*/
+ DECIMAL = /#{DIGIT10}+#+\.#*#{SUFFIX}|#{DIGIT10}+\.#{DIGIT10}*#*#{SUFFIX}|\.#{DIGIT10}+#*#{SUFFIX}|#{UINT10}#{EXP}/
+ UREAL10 = /#{UINT10}\/#{UINT10}|#{DECIMAL}|#{UINT10}/
+ UREAL16 = /#{UINT16}\/#{UINT16}|#{UINT16}/
+ UREAL8 = /#{UINT8}\/#{UINT8}|#{UINT8}/
+ UREAL2 = /#{UINT2}\/#{UINT2}|#{UINT2}/
+ REAL10 = /#{SIGN}#{UREAL10}/
+ REAL16 = /#{SIGN}#{UREAL16}/
+ REAL8 = /#{SIGN}#{UREAL8}/
+ REAL2 = /#{SIGN}#{UREAL2}/
+ IMAG10 = /i|#{UREAL10}i/
+ IMAG16 = /i|#{UREAL16}i/
+ IMAG8 = /i|#{UREAL8}i/
+ IMAG2 = /i|#{UREAL2}i/
+ COMPLEX10 = /#{REAL10}@#{REAL10}|#{REAL10}\+#{IMAG10}|#{REAL10}-#{IMAG10}|\+#{IMAG10}|-#{IMAG10}|#{REAL10}/
+ COMPLEX16 = /#{REAL16}@#{REAL16}|#{REAL16}\+#{IMAG16}|#{REAL16}-#{IMAG16}|\+#{IMAG16}|-#{IMAG16}|#{REAL16}/
+ COMPLEX8 = /#{REAL8}@#{REAL8}|#{REAL8}\+#{IMAG8}|#{REAL8}-#{IMAG8}|\+#{IMAG8}|-#{IMAG8}|#{REAL8}/
+ COMPLEX2 = /#{REAL2}@#{REAL2}|#{REAL2}\+#{IMAG2}|#{REAL2}-#{IMAG2}|\+#{IMAG2}|-#{IMAG2}|#{REAL2}/
+ NUM10 = /#{PREFIX10}?#{COMPLEX10}/
+ NUM16 = /#{PREFIX16}#{COMPLEX16}/
+ NUM8 = /#{PREFIX8}#{COMPLEX8}/
+ NUM2 = /#{PREFIX2}#{COMPLEX2}/
+ NUM = /#{NUM10}|#{NUM16}|#{NUM8}|#{NUM2}/
+
+ private
+ def scan_tokens tokens,options
+
+ state = :initial
+ ident_kind = IDENT_KIND
+
+ until eos?
+ kind = match = nil
+
+ case state
+ when :initial
+ if scan(/ \s+ | \\\n /x)
+ kind = :space
+ elsif scan(/['\(\[\)\]]|#\(/)
+ kind = :operator_fat
+ elsif scan(/;.*/)
+ kind = :comment
+ elsif scan(/#\\(?:newline|space|.?)/)
+ kind = :char
+ elsif scan(/#[ft]/)
+ kind = :pre_constant
+ elsif scan(/#{IDENTIFIER}/o)
+ kind = ident_kind[matched]
+ elsif scan(/\./)
+ kind = :operator
+ elsif scan(/"/)
+ tokens << [:open, :string]
+ state = :string
+ tokens << ['"', :delimiter]
+ next
+ elsif scan(/#{NUM}/o) and not matched.empty?
+ kind = :integer
+ elsif getch
+ kind = :error
+ end
+
+ when :string
+ if scan(/[^"\\]+/) or scan(/\\.?/)
+ kind = :content
+ elsif scan(/"/)
+ tokens << ['"', :delimiter]
+ tokens << [:close, :string]
+ state = :initial
+ next
+ else
+ raise_inspect "else case \" reached; %p not handled." % peek(1),
+ tokens, state
+ end
+
+ else
+ raise "else case reached"
+ end
+
+ match ||= matched
+ if $DEBUG and not kind
+ raise_inspect 'Error token %p in line %d' %
+ [[match, kind], line], tokens
+ end
+ raise_inspect 'Empty token', tokens, state unless match
+
+ tokens << [match, kind]
+
+ end # until eos
+
+ if state == :string
+ tokens << [:close, :string]
+ end
+
+ tokens
+
+ end #scan_tokens
+ end #class
+ end #module scanners
+end #module coderay
\ No newline at end of file
--- /dev/null
+module CodeRay
+module Scanners
+
+ load :html
+
+ # XML Scanner
+ #
+ # $Id$
+ #
+ # Currently this is the same scanner as Scanners::HTML.
+ class XML < HTML
+
+ register_for :xml
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+
+ # This module holds the Style class and its subclasses.
+ #
+ # See Plugin.
+ module Styles
+ extend PluginHost
+ plugin_path File.dirname(__FILE__), 'styles'
+
+ class Style
+ extend Plugin
+ plugin_host Styles
+
+ DEFAULT_OPTIONS = { }
+
+ end
+
+ end
+
+end
--- /dev/null
+module CodeRay
+module Styles
+
+ default :cycnus
+
+end
+end
--- /dev/null
+module CodeRay
+module Styles
+
+ class Cycnus < Style
+
+ register_for :cycnus
+
+ code_background = '#f8f8f8'
+ numbers_background = '#def'
+ border_color = 'silver'
+ normal_color = '#100'
+
+ CSS_MAIN_STYLES = <<-MAIN
+.CodeRay {
+ background-color: #{code_background};
+ border: 1px solid #{border_color};
+ font-family: 'Courier New', 'Terminal', monospace;
+ color: #{normal_color};
+}
+.CodeRay pre { margin: 0px }
+
+div.CodeRay { }
+
+span.CodeRay { white-space: pre; border: 0px; padding: 2px }
+
+table.CodeRay { border-collapse: collapse; width: 100%; padding: 2px }
+table.CodeRay td { padding: 2px 4px; vertical-align: top }
+
+.CodeRay .line_numbers, .CodeRay .no {
+ background-color: #{numbers_background};
+ color: gray;
+ text-align: right;
+}
+.CodeRay .line_numbers tt { font-weight: bold }
+.CodeRay .no { padding: 0px 4px }
+.CodeRay .code { width: 100% }
+
+ol.CodeRay { font-size: 10pt }
+ol.CodeRay li { white-space: pre }
+
+.CodeRay .code pre { overflow: auto }
+ MAIN
+
+ TOKEN_COLORS = <<-'TOKENS'
+.debug { color:white ! important; background:blue ! important; }
+
+.af { color:#00C }
+.an { color:#007 }
+.av { color:#700 }
+.aw { color:#C00 }
+.bi { color:#509; font-weight:bold }
+.c { color:#666; }
+
+.ch { color:#04D }
+.ch .k { color:#04D }
+.ch .dl { color:#039 }
+
+.cl { color:#B06; font-weight:bold }
+.co { color:#036; font-weight:bold }
+.cr { color:#0A0 }
+.cv { color:#369 }
+.df { color:#099; font-weight:bold }
+.di { color:#088; font-weight:bold }
+.dl { color:black }
+.do { color:#970 }
+.ds { color:#D42; font-weight:bold }
+.e { color:#666; font-weight:bold }
+.en { color:#800; font-weight:bold }
+.er { color:#F00; background-color:#FAA }
+.ex { color:#F00; font-weight:bold }
+.fl { color:#60E; font-weight:bold }
+.fu { color:#06B; font-weight:bold }
+.gv { color:#d70; font-weight:bold }
+.hx { color:#058; font-weight:bold }
+.i { color:#00D; font-weight:bold }
+.ic { color:#B44; font-weight:bold }
+
+.il { background: #eee }
+.il .il { background: #ddd }
+.il .il .il { background: #ccc }
+.il .idl { font-weight: bold; color: #888 }
+
+.in { color:#B2B; font-weight:bold }
+.iv { color:#33B }
+.la { color:#970; font-weight:bold }
+.lv { color:#963 }
+.oc { color:#40E; font-weight:bold }
+.of { color:#000; font-weight:bold }
+.op { }
+.pc { color:#038; font-weight:bold }
+.pd { color:#369; font-weight:bold }
+.pp { color:#579 }
+.pt { color:#339; font-weight:bold }
+.r { color:#080; font-weight:bold }
+
+.rx { background-color:#fff0ff }
+.rx .k { color:#808 }
+.rx .dl { color:#404 }
+.rx .mod { color:#C2C }
+.rx .fu { color:#404; font-weight: bold }
+
+.s { background-color:#fff0f0 }
+.s .s { background-color:#ffe0e0 }
+.s .s .s { background-color:#ffd0d0 }
+.s .k { color:#D20 }
+.s .dl { color:#710 }
+
+.sh { background-color:#f0fff0 }
+.sh .k { color:#2B2 }
+.sh .dl { color:#161 }
+
+.sy { color:#A60 }
+.sy .k { color:#A60 }
+.sy .dl { color:#630 }
+
+.ta { color:#070 }
+.tf { color:#070; font-weight:bold }
+.ts { color:#D70; font-weight:bold }
+.ty { color:#339; font-weight:bold }
+.v { color:#036 }
+.xt { color:#444 }
+ TOKENS
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+module Styles
+
+ class Murphy < Style
+
+ register_for :murphy
+
+ code_background = '#001129'
+ numbers_background = code_background
+ border_color = 'silver'
+ normal_color = '#C0C0C0'
+
+ CSS_MAIN_STYLES = <<-MAIN
+.CodeRay {
+ background-color: #{code_background};
+ border: 1px solid #{border_color};
+ font-family: 'Courier New', 'Terminal', monospace;
+ color: #{normal_color};
+}
+.CodeRay pre { margin: 0px; }
+
+div.CodeRay { }
+
+span.CodeRay { white-space: pre; border: 0px; padding: 2px; }
+
+table.CodeRay { border-collapse: collapse; width: 100%; padding: 2px; }
+table.CodeRay td { padding: 2px 4px; vertical-align: top; }
+
+.CodeRay .line_numbers, .CodeRay .no {
+ background-color: #{numbers_background};
+ color: gray;
+ text-align: right;
+}
+.CodeRay .line_numbers tt { font-weight: bold; }
+.CodeRay .no { padding: 0px 4px; }
+.CodeRay .code { width: 100%; }
+
+ol.CodeRay { font-size: 10pt; }
+ol.CodeRay li { white-space: pre; }
+
+.CodeRay .code pre { overflow: auto; }
+ MAIN
+
+ TOKEN_COLORS = <<-'TOKENS'
+.af { color:#00C; }
+.an { color:#007; }
+.av { color:#700; }
+.aw { color:#C00; }
+.bi { color:#509; font-weight:bold; }
+.c { color:#555; background-color: black; }
+
+.ch { color:#88F; }
+.ch .k { color:#04D; }
+.ch .dl { color:#039; }
+
+.cl { color:#e9e; font-weight:bold; }
+.co { color:#5ED; font-weight:bold; }
+.cr { color:#0A0; }
+.cv { color:#ccf; }
+.df { color:#099; font-weight:bold; }
+.di { color:#088; font-weight:bold; }
+.dl { color:black; }
+.do { color:#970; }
+.ds { color:#D42; font-weight:bold; }
+.e { color:#666; font-weight:bold; }
+.er { color:#F00; background-color:#FAA; }
+.ex { color:#F00; font-weight:bold; }
+.fl { color:#60E; font-weight:bold; }
+.fu { color:#5ed; font-weight:bold; }
+.gv { color:#f84; }
+.hx { color:#058; font-weight:bold; }
+.i { color:#66f; font-weight:bold; }
+.ic { color:#B44; font-weight:bold; }
+.il { }
+.in { color:#B2B; font-weight:bold; }
+.iv { color:#aaf; }
+.la { color:#970; font-weight:bold; }
+.lv { color:#963; }
+.oc { color:#40E; font-weight:bold; }
+.of { color:#000; font-weight:bold; }
+.op { }
+.pc { color:#08f; font-weight:bold; }
+.pd { color:#369; font-weight:bold; }
+.pp { color:#579; }
+.pt { color:#66f; font-weight:bold; }
+.r { color:#5de; font-weight:bold; }
+
+.rx { background-color:#221133; }
+.rx .k { color:#f8f; }
+.rx .dl { color:#f0f; }
+.rx .mod { color:#f0b; }
+.rx .fu { color:#404; font-weight: bold; }
+
+.s { background-color:#331122; }
+.s .s { background-color:#ffe0e0; }
+.s .s .s { background-color:#ffd0d0; }
+.s .k { color:#F88; }
+.s .dl { color:#f55; }
+
+.sh { background-color:#f0fff0; }
+.sh .k { color:#2B2; }
+.sh .dl { color:#161; }
+
+.sy { color:#Fc8; }
+.sy .k { color:#Fc8; }
+.sy .dl { color:#F84; }
+
+.ta { color:#070; }
+.tf { color:#070; font-weight:bold; }
+.ts { color:#D70; font-weight:bold; }
+.ty { color:#339; font-weight:bold; }
+.v { color:#036; }
+.xt { color:#444; }
+ TOKENS
+
+ end
+
+end
+end
--- /dev/null
+module CodeRay
+ class Tokens
+ ClassOfKind = Hash.new do |h, k|
+ h[k] = k.to_s
+ end
+ ClassOfKind.update with = {
+ :attribute_name => 'an',
+ :attribute_name_fat => 'af',
+ :attribute_value => 'av',
+ :attribute_value_fat => 'aw',
+ :bin => 'bi',
+ :char => 'ch',
+ :class => 'cl',
+ :class_variable => 'cv',
+ :color => 'cr',
+ :comment => 'c',
+ :constant => 'co',
+ :content => 'k',
+ :definition => 'df',
+ :delimiter => 'dl',
+ :directive => 'di',
+ :doc => 'do',
+ :doc_string => 'ds',
+ :entity => 'en',
+ :error => 'er',
+ :escape => 'e',
+ :exception => 'ex',
+ :float => 'fl',
+ :function => 'fu',
+ :global_variable => 'gv',
+ :hex => 'hx',
+ :include => 'ic',
+ :inline => 'il',
+ :inline_delimiter => 'idl',
+ :instance_variable => 'iv',
+ :integer => 'i',
+ :interpreted => 'in',
+ :label => 'la',
+ :local_variable => 'lv',
+ :modifier => 'mod',
+ :oct => 'oc',
+ :operator_fat => 'of',
+ :pre_constant => 'pc',
+ :pre_type => 'pt',
+ :predefined => 'pd',
+ :preprocessor => 'pp',
+ :regexp => 'rx',
+ :reserved => 'r',
+ :shell => 'sh',
+ :string => 's',
+ :symbol => 'sy',
+ :tag => 'ta',
+ :tag_fat => 'tf',
+ :tag_special => 'ts',
+ :type => 'ty',
+ :variable => 'v',
+ :xml_text => 'xt',
+
+ :ident => :NO_HIGHLIGHT, # 'id'
+ #:operator => 'op',
+ :operator => :NO_HIGHLIGHT, # 'op'
+ :space => :NO_HIGHLIGHT, # 'sp'
+ :plain => :NO_HIGHLIGHT,
+ }
+ ClassOfKind[:procedure] = ClassOfKind[:method] = ClassOfKind[:function]
+ ClassOfKind[:open] = ClassOfKind[:close] = ClassOfKind[:delimiter]
+ ClassOfKind[:nesting_delimiter] = ClassOfKind[:delimiter]
+ ClassOfKind[:escape] = ClassOfKind[:delimiter]
+ #ClassOfKind.default = ClassOfKind[:error] or raise 'no class found for :error!'
+ end
+end
\ No newline at end of file
--- /dev/null
+module CodeRay
+
+ # = Tokens
+ #
+ # The Tokens class represents a list of tokens returnd from
+ # a Scanner.
+ #
+ # A token is not a special object, just a two-element Array
+ # consisting of
+ # * the _token_ _kind_ (a Symbol representing the type of the token)
+ # * the _token_ _text_ (the original source of the token in a String)
+ #
+ # A token looks like this:
+ #
+ # [:comment, '# It looks like this']
+ # [:float, '3.1415926']
+ # [:error, 'äöü']
+ #
+ # Some scanners also yield some kind of sub-tokens, represented by special
+ # token texts, namely :open and :close .
+ #
+ # The Ruby scanner, for example, splits "a string" into:
+ #
+ # [
+ # [:open, :string],
+ # [:delimiter, '"'],
+ # [:content, 'a string'],
+ # [:delimiter, '"'],
+ # [:close, :string]
+ # ]
+ #
+ # Tokens is also the interface between Scanners and Encoders:
+ # The input is split and saved into a Tokens object. The Encoder
+ # then builds the output from this object.
+ #
+ # Thus, the syntax below becomes clear:
+ #
+ # CodeRay.scan('price = 2.59', :ruby).html
+ # # the Tokens object is here -------^
+ #
+ # See how small it is? ;)
+ #
+ # Tokens gives you the power to handle pre-scanned code very easily:
+ # You can convert it to a webpage, a YAML file, or dump it into a gzip'ed string
+ # that you put in your DB.
+ #
+ # Tokens' subclass TokenStream allows streaming to save memory.
+ class Tokens < Array
+
+ class << self
+
+ # Convert the token to a string.
+ #
+ # This format is used by Encoders.Tokens.
+ # It can be reverted using read_token.
+ def write_token text, type
+ if text.is_a? String
+ "#{type}\t#{escape(text)}\n"
+ else
+ ":#{text}\t#{type}\t\n"
+ end
+ end
+
+ # Read a token from the string.
+ #
+ # Inversion of write_token.
+ #
+ # TODO Test this!
+ def read_token token
+ type, text = token.split("\t", 2)
+ if type[0] == ?:
+ [text.to_sym, type[1..-1].to_sym]
+ else
+ [type.to_sym, unescape(text)]
+ end
+ end
+
+ # Escapes a string for use in write_token.
+ def escape text
+ text.gsub(/[\n\\]/, '\\\\\&')
+ end
+
+ # Unescapes a string created by escape.
+ def unescape text
+ text.gsub(/\\[\n\\]/) { |m| m[1,1] }
+ end
+
+ end
+
+ # Whether the object is a TokenStream.
+ #
+ # Returns false.
+ def stream?
+ false
+ end
+
+ # Iterates over all tokens.
+ #
+ # If a filter is given, only tokens of that kind are yielded.
+ def each kind_filter = nil, &block
+ unless kind_filter
+ super(&block)
+ else
+ super() do |text, kind|
+ next unless kind == kind_filter
+ yield text, kind
+ end
+ end
+ end
+
+ # Iterates over all text tokens.
+ # Range tokens like [:open, :string] are left out.
+ #
+ # Example:
+ # tokens.each_text_token { |text, kind| text.replace html_escape(text) }
+ def each_text_token
+ each do |text, kind|
+ next unless text.is_a? ::String
+ yield text, kind
+ end
+ end
+
+ # Encode the tokens using encoder.
+ #
+ # encoder can be
+ # * a symbol like :html oder :statistic
+ # * an Encoder class
+ # * an Encoder object
+ #
+ # options are passed to the encoder.
+ def encode encoder, options = {}
+ unless encoder.is_a? Encoders::Encoder
+ unless encoder.is_a? Class
+ encoder_class = Encoders[encoder]
+ end
+ encoder = encoder_class.new options
+ end
+ encoder.encode_tokens self, options
+ end
+
+
+ # Turn into a string using Encoders::Text.
+ #
+ # +options+ are passed to the encoder if given.
+ def to_s options = {}
+ encode :text, options
+ end
+
+
+ # Redirects unknown methods to encoder calls.
+ #
+ # For example, if you call +tokens.html+, the HTML encoder
+ # is used to highlight the tokens.
+ def method_missing meth, options = {}
+ Encoders[meth].new(options).encode_tokens self
+ end
+
+ # Returns the tokens compressed by joining consecutive
+ # tokens of the same kind.
+ #
+ # This can not be undone, but should yield the same output
+ # in most Encoders. It basically makes the output smaller.
+ #
+ # Combined with dump, it saves space for the cost of time.
+ #
+ # If the scanner is written carefully, this is not required -
+ # for example, consecutive //-comment lines could already be
+ # joined in one comment token by the Scanner.
+ def optimize
+ print ' Tokens#optimize: before: %d - ' % size if $DEBUG
+ last_kind = last_text = nil
+ new = self.class.new
+ for text, kind in self
+ if text.is_a? String
+ if kind == last_kind
+ last_text << text
+ else
+ new << [last_text, last_kind] if last_kind
+ last_text = text
+ last_kind = kind
+ end
+ else
+ new << [last_text, last_kind] if last_kind
+ last_kind = last_text = nil
+ new << [text, kind]
+ end
+ end
+ new << [last_text, last_kind] if last_kind
+ print 'after: %d (%d saved = %2.0f%%)' %
+ [new.size, size - new.size, 1.0 - (new.size.to_f / size)] if $DEBUG
+ new
+ end
+
+ # Compact the object itself; see optimize.
+ def optimize!
+ replace optimize
+ end
+
+ # Ensure that all :open tokens have a correspondent :close one.
+ #
+ # TODO: Test this!
+ def fix
+ # Check token nesting using a stack of kinds.
+ opened = []
+ for token, kind in self
+ if token == :open
+ opened.push kind
+ elsif token == :close
+ expected = opened.pop
+ if kind != expected
+ # Unexpected :close; decide what to do based on the kind:
+ # - token was opened earlier: also close tokens in between
+ # - token was never opened: delete the :close (skip with next)
+ next unless opened.rindex expected
+ tokens << [:close, kind] until (kind = opened.pop) == expected
+ end
+ end
+ tokens << [token, kind]
+ end
+ # Close remaining opened tokens
+ tokens << [:close, kind] while kind = opened.pop
+ tokens
+ end
+
+ def fix!
+ replace fix
+ end
+
+ # Makes sure that:
+ # - newlines are single tokens
+ # (which means all other token are single-line)
+ # - there are no open tokens at the end the line
+ #
+ # This makes it simple for encoders that work line-oriented,
+ # like HTML with list-style numeration.
+ def split_into_lines
+ raise NotImplementedError
+ end
+
+ def split_into_lines!
+ replace split_into_lines
+ end
+
+ # Dumps the object into a String that can be saved
+ # in files or databases.
+ #
+ # The dump is created with Marshal.dump;
+ # In addition, it is gzipped using GZip.gzip.
+ #
+ # The returned String object includes Undumping
+ # so it has an #undump method. See Tokens.load.
+ #
+ # You can configure the level of compression,
+ # but the default value 7 should be what you want
+ # in most cases as it is a good compromise between
+ # speed and compression rate.
+ #
+ # See GZip module.
+ def dump gzip_level = 7
+ require 'coderay/helpers/gzip_simple'
+ dump = Marshal.dump self
+ dump = dump.gzip gzip_level
+ dump.extend Undumping
+ end
+
+ # The total size of the tokens.
+ # Should be equal to the input size before
+ # scanning.
+ def text_size
+ size = 0
+ each_text_token do |t, k|
+ size + t.size
+ end
+ size
+ end
+
+ # The total size of the tokens.
+ # Should be equal to the input size before
+ # scanning.
+ def text
+ map { |t, k| t if t.is_a? ::String }.join
+ end
+
+ # Include this module to give an object an #undump
+ # method.
+ #
+ # The string returned by Tokens.dump includes Undumping.
+ module Undumping
+ # Calls Tokens.load with itself.
+ def undump
+ Tokens.load self
+ end
+ end
+
+ # Undump the object using Marshal.load, then
+ # unzip it using GZip.gunzip.
+ #
+ # The result is commonly a Tokens object, but
+ # this is not guaranteed.
+ def Tokens.load dump
+ require 'coderay/helpers/gzip_simple'
+ dump = dump.gunzip
+ @dump = Marshal.load dump
+ end
+
+ end
+
+
+ # = TokenStream
+ #
+ # The TokenStream class is a fake Array without elements.
+ #
+ # It redirects the method << to a block given at creation.
+ #
+ # This allows scanners and Encoders to use streaming (no
+ # tokens are saved, the input is highlighted the same time it
+ # is scanned) with the same code.
+ #
+ # See CodeRay.encode_stream and CodeRay.scan_stream
+ class TokenStream < Tokens
+
+ # Whether the object is a TokenStream.
+ #
+ # Returns true.
+ def stream?
+ true
+ end
+
+ # The Array is empty, but size counts the tokens given by <<.
+ attr_reader :size
+
+ # Creates a new TokenStream that calls +block+ whenever
+ # its << method is called.
+ #
+ # Example:
+ #
+ # require 'coderay'
+ #
+ # token_stream = CodeRay::TokenStream.new do |kind, text|
+ # puts 'kind: %s, text size: %d.' % [kind, text.size]
+ # end
+ #
+ # token_stream << [:regexp, '/\d+/']
+ # #-> kind: rexpexp, text size: 5.
+ #
+ def initialize &block
+ raise ArgumentError, 'Block expected for streaming.' unless block
+ @callback = block
+ @size = 0
+ end
+
+ # Calls +block+ with +token+ and increments size.
+ #
+ # Returns self.
+ def << token
+ @callback.call token
+ @size += 1
+ self
+ end
+
+ # This method is not implemented due to speed reasons. Use Tokens.
+ def text_size
+ raise NotImplementedError,
+ 'This method is not implemented due to speed reasons.'
+ end
+
+ # A TokenStream cannot be dumped. Use Tokens.
+ def dump
+ raise NotImplementedError, 'A TokenStream cannot be dumped.'
+ end
+
+ # A TokenStream cannot be optimized. Use Tokens.
+ def optimize
+ raise NotImplementedError, 'A TokenStream cannot be optimized.'
+ end
+
+ end
+
+
+ # Token name abbreviations
+ require 'coderay/token_classes'
+
+end
--- /dev/null
+.DS_Store
+test_app
+doc
\ No newline at end of file
--- /dev/null
+= EDGE
+
+* Samuel Williams (http://www.oriontransfer.co.nz/):
+ Thanks to Tekin for his patches.
+ Updated migrations system to tie in more closely with the current rails mechanism.
+ Rake task for updating database schema info
+ rake db:migrate:upgrade_plugin_migrations
+ Please see http://engines.lighthouseapp.com/projects/10178-engines-plugin/tickets/17 for more information.
+
+* Refactored the view loading to work with changes in Edge Rails
+
+* Fixed integration of plugin migrations with the new, default timestamped migrations in Edge Rails
+
+* Refactored tests into the plugin itself - the plugin can now generate its own test_app harness and run tests within it.
+
+
+= 2.0.0 - (ANOTHER) MASSIVE INTERNAL REFACTORING
+
+* Engines now conforms to the new plugin loading mechanism, delegating plugin load order and lots of other things to Rails itself.
+
+
+
+= 1.2.2
+
+* Added the ability to code mix different types of files, cleaning up the existing code-mixing implementation slightly (Ticket #271)
+
+
+= 1.2.1
+
+* Added documentation to clarify some of the issues with Rails unloading classes that aren't required using "require_dependency" (Ticket #266)
+
+* Fixed a bug where test_help was being loaded when it wasn't needed, and was actually causing problems (Ticket #265)
+
+
+= 1.2.0 - MASSIVE INTERNAL REFACTORING
+
+* !!!Support for Rails < 1.2 has been dropped!!!; if you are using Rails =< 1.1.6, please use Engines 1.1.6, available from http://svn.rails-engines.org/engines/tags/rel_1.1.6
+
+* Engines are dead! Long live plugins! There is now no meaningful notion of an engine - all plugins can take advantage of the more powerful features that the engines plugin provides by including app directories, etc.
+
+* Init_engine.rb is no longer used; please use the plugin-standard init.rb instead.
+
+* Engines.start is no longer required; please use the config.plugins array provided by Rails instead
+
+* To get the most benefit from Engines, set config.plugins to ["engines", "*"] to load the engines plugin first, and then all other plugins in their normal order after.
+
+* Access all loaded plugins via the new Rails.plugins array, and by name using Rails.plugins[:plugin_name].
+
+* Access plugin metadata loaded automatically from about.yml: Rails.plugins[:name].about. Plugin#version is provided directly, for easy access.
+
+* Module.config is has been removed - use mattr_accessor instead, and initialize your default values via the init.rb mechanism.
+
+* Public asset helpers have been rewritten; instead of engine_stylesheet, now use stylesheet_link_tag :name, :plugin => "plugin_name"
+
+* Plugin migrations have been reworked to integrate into the main migration stream. Please run script/generate plugin_migration to create plugin migrations in your main application.
+
+* The fixture method for loading fixtures against any class has been removed; instead, engines will now provide a mechanism for loading fixtures from all plugins, by mirroring fixtures into a common location.
+
+* All references to engines have been removed; For example, any rake tasks which applied to engines now apply to all plugins. The default Rails rake tasks for plugins are overridden where necessary.
+
+* Layouts can now be shared via plugins - inspiration gratefully taken from PluginAWeek's plugin_routing :)
+
+* Actual routing from plugins is now possible, by including routes.rb in your plugin directory and using the from_plugin method in config/routes.rb (Ticket #182)
+
+* Controllers are no longer loaded twice if they're not present in the normal app/ directory (Ticket #177)
+
+* The preferred location for javascripts/stylesheets/etc is now 'assets' rather than 'public'
+
+* Ensure that plugins started before routing have their controllers appropriately added to config.controller_paths (Ticket #258)
+
+* Removed Engines.version - it's not longer relevant, now we're loading version information from about.yml files.
+
+* Added a huge amount of documentation to all new modules.
+
+* Added new warning message if installation of engines 1.2.x is attempted in a Rails 1.1.x application
+
+* Added details of the removal of the config method to UPGRADING
+
+* Removed the plugins:info rake task in favour of adding information to script/about via the Rails::Info module (Ticket #261)
+
+* Improved handling of testing and documentation tasks for plugins
+
+
+
+= 1.1.4
+
+* Fixed creation of multipart emails (Ticket #190)
+
+* Added a temporary fix to the code-mixing issue. In your engine's test/test_helper.rb, please add the following lines:
+
+ # Ensure that the code mixing and view loading from the application is disabled
+ Engines.disable_app_views_loading = true
+ Engines.disable_app_code_mixing = true
+
+ which will prevent code mixing for controllers and helpers, and loading views from the application. One thing to remember is to load any controllers/helpers using 'require_or_load' in your tests, to ensure that the engine behaviour is respected (Ticket #135)
+
+* Added tasks to easily test engines individually (Ticket #120)
+
+* Fixture extensions will now fail with an exception if the corresponding class cannot be loaded (Ticket #138)
+
+* Patch for new routing/controller loading in Rails 1.1.6. The routing code is now replaced with the contents of config.controller_paths, along with controller paths from any started engines (Ticket #196)
+
+* Rails' Configuration instance is now stored, and available from all engines and plugins.
+
+
+
+= 1.1.3
+
+* Fixed README to show 'models' rather than 'model' class (Ticket #167)
+* Fixed dependency loading to work with Rails 1.1.4 (Ticket #180)
+
+
+
+= 1.1.2
+
+* Added better fix to version checking (Ticket #130, jdell@gbdev.com).
+
+* Fixed generated init_engine.rb so that VERSION module doesn't cause probems (Ticket #131, japgolly@gmail.com)
+
+* Fixed error with Rails 1.0 when trying to ignore the engine_schema_info table (Ticket #132, snowblink@gmail.com)
+
+* Re-added old style rake tasks (Ticket #133)
+
+* No longer adding all subdirectories of <engine>/app or <engine>/lib, as this can cause issues when files are grouped in modules (Ticket #149, kasatani@gmail.com)
+
+* Fixed engine precidence ordering for Rails 1.1 (Ticket #146)
+
+* Added new Engines.each method to assist in processing the engines in the desired order (Ticket #146)
+
+* Fixed annoying error message at appears when starting the console in development mode (Ticket #134)
+
+* Engines is now super-careful about loading the correct version of Rails from vendor (Ticket #154)
+
+
+
+= 1.1.1
+
+* Fixed migration rake task failing when given a specific version (Ticket #115)
+
+* Added new rake task "test:engines" which will test engines (and other plugins) but ensure that the test database is cloned from development beforehand (Ticket #125)
+
+* Fixed issue where 'engine_schema_info' table was included in schema dumps (Ticket #87)
+
+* Fixed multi-part emails (Ticket #121)
+
+* Added an 'install.rb' file to new engines created by the bundled generator, which installs the engines plugin automatically if it doesn't already exist (Ticket #122)
+
+* Added a default VERSION module to generated engines (Ticket #123)
+
+* Refactored copying of engine's public files to a method of an Engine instance. You can now call Engines.get(:engine_name).copy_public_files (Ticket #108)
+
+* Changed engine generator templates from .rb files to .erb files (Ticket #106)
+
+* Fixed the test_helper.erb file to use the correct testing extensions and not load any schema - the schema will be cloned automatically via rake test:engines
+
+* Fixed problem when running with Rails 1.1.1 where version wasn't determined correctly (Ticket #129)
+
+* Fixed bug preventing engines from loading when both Rails 1.1.0 and 1.1.1 gems are installed and in use.
+
+* Updated version (d'oh!)
+
+
+
+= 1.1.0
+
+* Improved regexp matching for Rails 1.0 engines with peculiar paths
+
+* Engine instance objects can be accessed via Engines[:name], an alias for Engines.get(:name) (Ticket #99)
+
+* init_engine.rb is now processed as the final step in the Engine.start process, so it can access files within the lib directory, which is now in the $LOAD_PATH at that point. (Ticket #99)
+
+* Clarified MIT license (Ticket #98)
+
+* Updated Rake tasks to integrate smoothly with Rails 1.1 namespaces
+
+* Changed the version to "1.1.0 (svn)"
+
+* Added more information about using the plugin with Edge Rails to the README
+
+* moved extensions into lib/engines/ directory to enable use of Engines module in extension code.
+
+* Added conditional require_or_load method which attempts to detect the current Rails version. To use the Edge Rails version of the loading mechanism, add the line:
+
+* Engines.config :edge, true
+
+* to your environment.rb file.
+
+* Merged changes from /branches/edge and /branches/rb_1.0 into /trunk
+
+* engine_schema_info now respects the prefix/suffixes set for ActiveRecord::Base (Ticket #67)
+
+* added ActiveRecord::Base.wrapped_table_name(name) method to assist in determining the correct table name
+
+
+
+= 1.0.6
+
+* Added ability to determine version information for engines: rake engine_info
+
+* Added a custom logger for the Engines module, to stop pollution of the Rails logs.
+
+* Added some more tests (in particular, see rails_engines/applications/engines_test).
+
+* Another attempt at solving Ticket #53 - controllers and helpers should now be loadable from modules, and if a full path (including RAILS_ROOT/ENGINES_ROOT) is given, it should be safely stripped from the require filename such that corresponding files can be located in any active engines. In other words, controller/helper overloading should now completely work, even if the controllers/helpers are in modules.
+
+* Added (finally) patch from Ticket #22 - ActionMailer helpers should now load
+
+* Removed support for Engines.start :engine, :engine_name => 'whatever'. It was pointless.
+
+* Fixed engine name referencing; engine_stylesheet/engine_javascript can now happily use shorthand engine names (i.e. :test == :test_engine) (Ticket #45)
+
+* Fixed minor documentation error ('Engine.start' ==> 'Engines.start') (Ticket #57)
+
+* Fixed double inclusion of RAILS_ROOT in engine_migrate rake task (Ticket #61)
+
+* Added ability to force config values even if given as a hash (Ticket #62)
+
+
+
+= 1.0.5
+
+* Fixed bug stopping fixtures from loading with PostgreSQL
+
+
+
+= 1.0.4
+
+* Another attempt at loading controllers within modules (Ticket #56)
+
+
+
+= 1.0.3
+
+* Fixed serious dependency bug stopping controllers being loaded (Ticket #56)
+
+
+
+= 1.0.2
+
+* Fixed bug with overloading controllers in modules from /app directory
+
+* Fixed exception thrown when public files couldn't be created; exception is now logged (Ticket #52)
+
+* Fixed problem with generated test_helper.rb file via File.expand_path (Ticket #50)
+
+
+
+= 1.0.1
+
+* Added engine generator for creation of new engines
+
+* Fixed 'Engine' typo in README
+
+* Fixed bug in fixtures extensions
+
+* Fixed /lib path management bug
+
+* Added method to determine public directory location from Engine object
+
+* Fixed bug in the error message in get_engine_dir()
+
+* Added proper component loading
+
+* Added preliminary tests for the config() methods module
+
+
+
+= pre-v170
+
+* Fixed copyright notices to point to DHH, rather than me.
+
+* Moved extension require statements into lib/engines.rb, so the will be loaded if another module/file calls require 'engines
+
+* Added a CHANGELOG file (this file)
--- /dev/null
+Copyright (c) 2008 James Adam
+
+The MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
--- /dev/null
+The engines plugin enhances Rails' own plugin framework, making it simple to share controllers, helpers, models, public assets, routes and migrations in plugins.
+
+For more information, see http://rails-engines.org
+
+= Using the plugin
+
+Once you've installed the engines plugin, you'll need to add a single line to the top of config/environment.rb:
+
+ require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
+
+You should add this line just below the require for Rails' own boot.rb file. This will enabled the enhanced plugin loading mechanism automatically for you (i.e. you don't need to set config.plugin_loader manually).
+
+With that aside, you're now ready to start using more powerful plugins in your application. Read on to find out more about what the engines plugin enables.
+
+
+== Better plugins
+
+In addition to the regular set of plugin-supported files (lib, init.rb, tasks, generators, tests), plugins can carry the following when the engines plugin is also installed.
+
+
+=== Controllers, Helpers, and Views
+
+Include these files in an <tt>app</tt> directory just like you would in a normal Rails application. If you need to override a method, view or partial, create the corresponding file in your main <tt>app</tt> directory and it will be used instead.
+
+* Controllers & Helpers: See Engines::RailsExtensions::Dependencies for more information.
+* Views: now handled almost entirely by ActionView itself (see Engines::Plugin#add_plugin_view_paths for more information)
+
+=== Models
+
+Model code can similarly be placed in an <tt>app/models/</tt> directory. Unfortunately, it's not possible to automatically override methods within a model; if your application needs to change the way a model behaves, consider creating a subclass, or replacing the model entirely within your application's <tt>app/models/</tt> directory. See Engines::RailsExtensions::Dependencies for more information.
+
+IMPORTANT NOTE: when you load code from within plugins, it is typically not handled well by Rails in terms of unloading and reloading changes. Look here for more information - http://rails-engines.org/development/common-issues-when-overloading-code-from-plugins/
+
+=== Routes
+
+Include your route declarations in a <tt>routes.rb</tt> file at the root of your plugins, e.g.:
+
+ connect "/my/url", :controller => "some_controller"
+ my_named_route "do_stuff", :controller => "blah", :action => "stuff"
+ # etc.
+
+You can then load these files into your application by declaring their inclusion in the application's <tt>config/routes.rb</tt>:
+
+ map.from_plugin :plugin_name
+
+See Engines::RailsExtensions::Routing for more information.
+
+=== Migrations
+
+Migrations record the changes in your database as your application evolves. With engines 1.2, migrations from plugins can also join in this evolution as first-class entities. To add migrations to a plugin, include a <tt>db/migrate/</tt> folder and add migrations there as normal. These migrations can then be integrated into the main flow of database evolution by running the plugin_migration generator:
+
+ script/generate plugin_migration
+
+This will produce a migration in your application. Running this migration (via <tt>rake db:migrate</tt>, as normal) will migrate the database according to the latest migrations in each plugin. See Engines::RailsExtensions::Migrations for more information.
+
+
+=== More powerful Rake tasks
+
+The engines plugin enhances and adds to the suite of default rake tasks for working with plugins. The <tt>doc:plugins</tt> task now includes controllers, helpers and models under <tt>app</tt>, and anything other code found under the plugin's <tt>code_paths</tt> attribute. New testing tasks have been added to run unit, functional and integration tests from plugins, whilst making it easier to load fixtures from plugins. See Engines::Testing for more details about testing, and run
+
+ rake -T
+
+to see the set of rake tasks available.
+
+= Testing the engines plugin itself
+
+Because of the way the engines plugin modifies Rails, the simplest way to consistently test it against multiple versions is by generating a test harness application - a full Rails application that includes tests to verify the engines plugin behaviour in a real, running environment.
+
+Run the tests like this:
+
+ $ cd engines
+ $ rake test
+
+This will generate a test_app directory within the engines plugin (using the default 'rails' command), import tests and code into that application and then run the test suite.
+
+If you wish to test against a specific version of Rails, run the tests with the RAILS environment variable set to the local directory containing your Rails checkout
+
+ $ rake test RAILS=/Users/james/Code/rails_edge_checkout
+
+Alternatively, you can clone the latest version of Rails ('edge rails') from github like so:
+
+ $ rake test RAILS=edge
+
--- /dev/null
+require 'rake'
+require 'rake/rdoctask'
+require 'tmpdir'
+
+task :default => :doc
+
+desc 'Generate documentation for the engines plugin.'
+Rake::RDocTask.new(:doc) do |doc|
+ doc.rdoc_dir = 'doc'
+ doc.title = 'Engines'
+ doc.main = "README"
+ doc.rdoc_files.include("README", "CHANGELOG", "MIT-LICENSE")
+ doc.rdoc_files.include('lib/**/*.rb')
+ doc.options << '--line-numbers' << '--inline-source'
+end
+
+desc 'Run the engine plugin tests within their test harness'
+task :cruise do
+ # checkout the project into a temporary directory
+ version = "rails_2.0"
+ test_dir = "#{Dir.tmpdir}/engines_plugin_#{version}_test"
+ puts "Checking out test harness for #{version} into #{test_dir}"
+ `svn co http://svn.rails-engines.org/test/engines/#{version} #{test_dir}`
+
+ # run all the tests in this project
+ Dir.chdir(test_dir)
+ load 'Rakefile'
+ puts "Running all tests in test harness"
+ ['db:migrate', 'test', 'test:plugins'].each do |t|
+ Rake::Task[t].invoke
+ end
+end
+
+task :clean => [:clobber_doc, "test:clean"]
+
+namespace :test do
+
+ # Yields a block with STDOUT and STDERR silenced. If you *really* want
+ # to output something, the block is yielded with the original output
+ # streams, i.e.
+ #
+ # silence do |o, e|
+ # puts 'hello!' # no output produced
+ # o.puts 'hello!' # output on STDOUT
+ # end
+ #
+ # (based on silence_stream in ActiveSupport.)
+ def silence
+ yield(STDOUT, STDERR) if ENV['VERBOSE']
+ streams = [STDOUT, STDERR]
+ actual_stdout = STDOUT.dup
+ actual_stderr = STDERR.dup
+ streams.each do |s|
+ s.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
+ s.sync = true
+ end
+ yield actual_stdout, actual_stderr
+ ensure
+ STDOUT.reopen(actual_stdout)
+ STDERR.reopen(actual_stderr)
+ end
+
+ def test_app_dir
+ File.join(File.dirname(__FILE__), 'test_app')
+ end
+
+ def run(cmd)
+ cmd = cmd.join(" && ") if cmd.is_a?(Array)
+ system(cmd) || raise("failed running '#{cmd}'")
+ end
+
+ desc 'Remove the test application'
+ task :clean do
+ FileUtils.rm_r(test_app_dir) if File.exist?(test_app_dir)
+ end
+
+ desc 'Build the test rails application (use RAILS=[edge,<directory>] to test against specific version)'
+ task :generate_app do
+ silence do |out, err|
+ out.puts "> Creating test application at #{test_app_dir}"
+
+ if ENV['RAILS']
+ vendor_dir = File.join(test_app_dir, 'vendor')
+ FileUtils.mkdir_p vendor_dir
+
+ if ENV['RAILS'] == 'edge'
+ out.puts " Cloning Edge Rails from GitHub"
+ run "cd #{vendor_dir} && git clone --depth 1 git://github.com/rails/rails.git"
+ elsif ENV['RAILS'] =~ /\d\.\d\.\d/
+ if ENV['CURL']
+ out.puts " Cloning Rails Tag #{ENV['RAILS']} from GitHub using curl and tar"
+ run ["cd #{vendor_dir}",
+ "mkdir rails",
+ "cd rails",
+ "curl -s -L http://github.com/rails/rails/tarball/#{ENV['RAILS']} | tar xzv --strip-components 1"]
+ else
+ out.puts " Cloning Rails Tag #{ENV['RAILS']} from GitHub (can be slow - set CURL=true to use curl)"
+ run ["cd #{vendor_dir}",
+ "git clone git://github.com/rails/rails.git",
+ "cd rails",
+ "git pull",
+ "git checkout v#{ENV['RAILS']}"]
+ end
+ elsif File.exist?(ENV['RAILS'])
+ out.puts " Linking rails from #{ENV['RAILS']}"
+ run "cd #{vendor_dir} && ln -s #{ENV['RAILS']} rails"
+ else
+ raise "Couldn't build test application from '#{ENV['RAILS']}'"
+ end
+
+ out.puts " generating rails default directory structure"
+ run "ruby #{File.join(vendor_dir, 'rails', 'railties', 'bin', 'rails')} #{test_app_dir}"
+ else
+ version = `rails --version`.chomp.split.last
+ out.puts " building rails using the 'rails' command (rails version: #{version})"
+ run "rails #{test_app_dir}"
+ end
+
+ # get the database config and schema in place
+ out.puts " writing database.yml"
+ require 'yaml'
+ File.open(File.join(test_app_dir, 'config', 'database.yml'), 'w') do |f|
+ f.write(%w(development test).inject({}) do |h, env|
+ h[env] = {"adapter" => "sqlite3", "database" => "engines_#{env}.sqlite3"} ; h
+ end.to_yaml)
+ end
+ out.puts " installing exception_notification plugin"
+ run "cd #{test_app_dir} && ./script/plugin install git://github.com/rails/exception_notification.git"
+ end
+ end
+
+ # We can't link the plugin, as it needs to be present for script/generate to find
+ # the plugin generator.
+ # TODO: find and +1/create issue for loading generators from symlinked plugins
+ desc 'Mirror the engines plugin into the test application'
+ task :copy_engines_plugin do
+ puts "> Copying engines plugin into test application"
+ engines_plugin = File.join(test_app_dir, "vendor", "plugins", "engines")
+ FileUtils.rm_r(engines_plugin) if File.exist?(engines_plugin)
+ FileUtils.mkdir_p(engines_plugin)
+ FileList["*"].exclude("test_app").each do |file|
+ FileUtils.cp_r(file, engines_plugin)
+ end
+ end
+
+ def insert_line(line, options)
+ line = line + "\n"
+ target_file = File.join(test_app_dir, options[:into])
+ lines = File.readlines(target_file)
+ return if lines.include?(line)
+
+ if options[:after]
+ if options[:after].is_a?(String)
+ after_line = options[:after] + "\n"
+ else
+ after_line = lines.find { |l| l =~ options[:after] }
+ raise "couldn't find a line matching #{options[:after].inspect} in #{target_file}" unless after_line
+ end
+ index = lines.index(after_line)
+ raise "couldn't find line '#{after_line}' in #{target_file}" unless index
+ lines.insert(index + 1, line)
+ else
+ lines << line
+ end
+ File.open(target_file, 'w') { |f| f.write lines.join }
+ end
+
+ def mirror_test_files(src, dest=nil)
+ destination_dir = File.join(*([test_app_dir, dest].compact))
+ FileUtils.cp_r(File.join(File.dirname(__FILE__), 'test', src), destination_dir)
+ end
+
+ desc 'Update the plugin and tests files in the test application from the plugin'
+ task :mirror_engine_files => [:test_app, :copy_engines_plugin] do
+ puts "> Tweaking generated application to be suitable for testing"
+
+ # Replace the Rails plugin loader with the engines one.
+ insert_line("require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')",
+ :into => 'config/environment.rb',
+ :after => "require File.join(File.dirname(__FILE__), 'boot')")
+
+ # Add the engines test helper to handle fixtures & stuff.
+ insert_line("require 'engines_test_helper'", :into => 'test/test_helper.rb')
+
+ # Run engine plugin tests when running the application
+ insert_line("task :test => ['test:engines:all']", :into => 'Rakefile')
+
+ # We want exceptions to be raised
+ insert_line("def rescue_action(e) raise e end;",
+ :into => "app/controllers/application_controller.rb",
+ :after => "class ApplicationController < ActionController::Base")
+
+ # We need this method to test where actions are being rendered from.
+ insert_line("include RenderInformation",
+ :into => "app/controllers/application_controller.rb",
+ :after => "class ApplicationController < ActionController::Base")
+
+ puts "> Mirroring test application files into #{test_app_dir}"
+ mirror_test_files('app')
+ mirror_test_files('lib')
+ mirror_test_files('plugins', 'vendor')
+ mirror_test_files('unit', 'test')
+ mirror_test_files('functional', 'test')
+ end
+
+ desc 'Prepare the engines test environment'
+ task :test_app do
+ version_tag = File.join(test_app_dir, 'RAILS_VERSION')
+ existing_version = File.read(version_tag).chomp rescue 'unknown'
+ if existing_version == ENV['RAILS']
+ puts "> Reusing existing test application (#{ENV['RAILS']})"
+ else
+ puts "> Recreating test application"
+ Rake::Task["test:clean"].invoke
+ Rake::Task["test:generate_app"].invoke
+
+ File.open(version_tag, "w") { |f| f.write ENV['RAILS'] }
+ end
+ end
+end
+
+task :test => "test:mirror_engine_files" do
+ puts "> Loading the test application environment and running tests"
+ # We use exec here to replace the current running rake process
+ exec("cd #{test_app_dir} && rake db:migrate && rake")
+end
--- /dev/null
+author: James Adam
+email: james.adam@gmail.com
+homepage: http://www.rails-engines.org
+summary: Enhances the plugin mechanism to perform more flexible sharing
+description: The Rails Engines plugin allows the sharing of almost any type of code or asset that you could use in a Rails application, including controllers, models, stylesheets, and views.
+license: MIT
+version: 2.3.2
\ No newline at end of file
--- /dev/null
+begin
+ require 'rails/version'
+ unless Rails::VERSION::MAJOR >= 2 && Rails::VERSION::MINOR >= 3 && Rails::VERSION::TINY >= 2
+ raise "This version of the engines plugin requires Rails 2.3.2 or later!"
+ end
+end
+
+require File.join(File.dirname(__FILE__), 'lib/engines')
+
+# initialize Rails::Configuration with our own default values to spare users
+# some hassle with the installation and keep the environment cleaner
+
+{ :default_plugin_locators => (defined?(Gem) ? [Rails::Plugin::GemLocator] : []).push(Engines::Plugin::FileSystemLocator),
+ :default_plugin_loader => Engines::Plugin::Loader,
+ :default_plugins => [:engines, :all] }.each do |name, default|
+ Rails::Configuration.send(:define_method, name) { default }
+end
\ No newline at end of file
--- /dev/null
+Description:
+ The plugin migration generator assists in working with schema additions
+ required by plugins. Instead of running migrations from plugins directly,
+ the generator creates a regular Rails migration which will be responsible
+ for migrating the plugins from their current version to the latest version
+ installed.
+
+ This is important because the set of application migrations remains an
+ accurate record of the state of the database, even as plugins are installed
+ and removed during the development process.
+
+Example:
+ ./script/generate plugin_migration [<plugin_name> <another_plugin_name> ...]
+
+ This will generate:
+
+ RAILS_ROOT
+ |- db
+ |-migrate
+ |- xxx_plugin_migrations.rb
+
+ which contains the migrations for the given plugin(s).
+
+
+Advanced Usage:
+
+There may be situations where you need *complete* control over the migrations
+of plugins in your application, migrating a certainly plugin down to X, and
+another plugin up to Y, where neither X or Y are the latest migrations for those
+plugins.
+
+For those unfortunate few, I have two pieces of advice:
+
+ 1. Why? This is a code smell [http://c2.com/xp/CodeSmell.html].
+
+ 2. Well, OK. Don't panic. You can completely control plugin migrations by
+ creating your own migrations. To manually migrate a plugin to a specific
+ version, simply use
+
+ Engines.plugins[:your_plugin_name].migrate(version)
+
+ where version is the integer of the migration this plugin should end
+ up at.
+
+With great power comes great responsibility. Use this wisely.
\ No newline at end of file
--- /dev/null
+# Generates a migration which migrates all plugins to their latest versions
+# within the database.
+class PluginMigrationGenerator < Rails::Generator::Base
+
+ # 255 characters max for Windows NTFS (http://en.wikipedia.org/wiki/Filename)
+ # minus 14 for timestamp, minus some extra chars for dot, underscore, file
+ # extension. So let's have 230.
+ MAX_FILENAME_LENGTH = 230
+
+ def initialize(runtime_args, runtime_options={})
+ super
+ @options = {:assigns => {}}
+ ensure_schema_table_exists
+ get_plugins_to_migrate(runtime_args)
+
+ if @plugins_to_migrate.empty?
+ puts "All plugins are migrated to their latest versions"
+ exit(0)
+ end
+
+ @options[:migration_file_name] = build_migration_name
+ @options[:assigns][:class_name] = build_migration_name.classify
+ end
+
+ def manifest
+ record do |m|
+ m.migration_template 'plugin_migration.erb', 'db/migrate', @options
+ end
+ end
+
+ protected
+
+ # Create the schema table if it doesn't already exist.
+ def ensure_schema_table_exists
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
+ end
+
+ # Determine all the plugins which have migrations that aren't present
+ # according to the plugin schema information from the database.
+ def get_plugins_to_migrate(plugin_names)
+
+ # First, grab all the plugins which exist and have migrations
+ @plugins_to_migrate = if plugin_names.empty?
+ Engines.plugins
+ else
+ plugin_names.map do |name|
+ Engines.plugins[name] ? Engines.plugins[name] : raise("Cannot find the plugin '#{name}'")
+ end
+ end
+
+ @plugins_to_migrate.reject! { |p| !p.respond_to?(:latest_migration) || p.latest_migration.nil? }
+
+ # Then find the current versions from the database
+ @current_versions = {}
+ @plugins_to_migrate.each do |plugin|
+ @current_versions[plugin.name] = Engines::Plugin::Migrator.current_version(plugin)
+ end
+
+ # Then find the latest versions from their migration directories
+ @new_versions = {}
+ @plugins_to_migrate.each do |plugin|
+ @new_versions[plugin.name] = plugin.latest_migration
+ end
+
+ # Remove any plugins that don't need migration
+ @plugins_to_migrate.map { |p| p.name }.each do |name|
+ @plugins_to_migrate.delete(Engines.plugins[name]) if @current_versions[name] == @new_versions[name]
+ end
+
+ @options[:assigns][:plugins] = @plugins_to_migrate
+ @options[:assigns][:new_versions] = @new_versions
+ @options[:assigns][:current_versions] = @current_versions
+ end
+
+ # Returns a migration name. If the descriptive migration name based on the
+ # plugin names involved is shorter than 230 characters that one will be
+ # used. Otherwise a shorter name will be returned.
+ def build_migration_name
+ returning descriptive_migration_name do |name|
+ name.replace short_migration_name if name.length > MAX_FILENAME_LENGTH
+ end
+ end
+
+ # Construct a unique migration name based on the plugins involved and the
+ # versions they should reach after this migration is run. The name constructed
+ # needs to be lowercase
+ def descriptive_migration_name
+ @plugins_to_migrate.map do |plugin|
+ "#{plugin.name}_to_version_#{@new_versions[plugin.name]}"
+ end.join("_and_").downcase
+ end
+
+ # Short migration name that will be used if the descriptive_migration_name
+ # exceeds 230 characters
+ def short_migration_name
+ 'plugin_migrations'
+ end
+end
\ No newline at end of file
--- /dev/null
+class <%= class_name %> < ActiveRecord::Migration
+ def self.up
+ <%- plugins.each do |plugin| -%>
+ Engines.plugins["<%= plugin.name %>"].migrate(<%= new_versions[plugin.name] %>)
+ <%- end -%>
+ end
+
+ def self.down
+ <%- plugins.each do |plugin| -%>
+ Engines.plugins["<%= plugin.name %>"].migrate(<%= current_versions[plugin.name] %>)
+ <%- end -%>
+ end
+end
--- /dev/null
+# Only call Engines.init once, in the after_initialize block so that Rails
+# plugin reloading works when turned on
+config.after_initialize do
+ Engines.init(initializer) if defined? :Engines
+end
\ No newline at end of file
--- /dev/null
+require 'active_support'
+require File.join(File.dirname(__FILE__), 'engines/plugin')
+require File.join(File.dirname(__FILE__), 'engines/plugin/list')
+require File.join(File.dirname(__FILE__), 'engines/plugin/loader')
+require File.join(File.dirname(__FILE__), 'engines/plugin/locator')
+require File.join(File.dirname(__FILE__), 'engines/assets')
+require File.join(File.dirname(__FILE__), 'engines/rails_extensions/rails')
+
+# == Parameters
+#
+# The Engines module has a number of public configuration parameters:
+#
+# [+public_directory+] The directory into which plugin assets should be
+# mirrored. Defaults to <tt>RAILS_ROOT/public/plugin_assets</tt>.
+# [+schema_info_table+] The table to use when storing plugin migration
+# version information. Defaults to +plugin_schema_info+.
+#
+# Additionally, there are a few flags which control the behaviour of
+# some of the features the engines plugin adds to Rails:
+#
+# [+disable_application_view_loading+] A boolean flag determining whether
+# or not views should be loaded from
+# the main <tt>app/views</tt> directory.
+# Defaults to false; probably only
+# useful when testing your plugin.
+# [+disable_application_code_loading+] A boolean flag determining whether
+# or not to load controllers/helpers
+# from the main +app+ directory,
+# if corresponding code exists within
+# a plugin. Defaults to false; again,
+# probably only useful when testing
+# your plugin.
+# [+disable_code_mixing+] A boolean flag indicating whether all plugin
+# copies of a particular controller/helper should
+# be loaded and allowed to override each other,
+# or if the first matching file should be loaded
+# instead. Defaults to false.
+#
+module Engines
+ # The set of all loaded plugins
+ mattr_accessor :plugins
+ self.plugins = Engines::Plugin::List.new
+
+ # List of extensions to load, can be changed in init.rb before calling Engines.init
+ mattr_accessor :rails_extensions
+ self.rails_extensions = %w(asset_helpers form_tag_helpers migrations dependencies)
+
+ # The name of the public directory to mirror public engine assets into.
+ # Defaults to <tt>RAILS_ROOT/public/plugin_assets</tt>.
+ mattr_accessor :public_directory
+ self.public_directory = File.join(RAILS_ROOT, 'public', 'plugin_assets')
+
+ # The table in which to store plugin schema information. Defaults to
+ # "plugin_schema_info".
+ mattr_accessor :schema_info_table
+ self.schema_info_table = "plugin_schema_info"
+
+ #--
+ # These attributes control the behaviour of the engines extensions
+ #++
+
+ # Set this to true if views should *only* be loaded from plugins
+ mattr_accessor :disable_application_view_loading
+ self.disable_application_view_loading = false
+
+ # Set this to true if controller/helper code shouldn't be loaded
+ # from the application
+ mattr_accessor :disable_application_code_loading
+ self.disable_application_code_loading = false
+
+ # Set this to true if code should not be mixed (i.e. it will be loaded
+ # from the first valid path on $LOAD_PATH)
+ mattr_accessor :disable_code_mixing
+ self.disable_code_mixing = false
+
+ # This is used to determine which files are candidates for the "code
+ # mixing" feature that the engines plugin provides, where classes from
+ # plugins can be loaded, and then code from the application loaded
+ # on top of that code to override certain methods.
+ mattr_accessor :code_mixing_file_types
+ self.code_mixing_file_types = %w(controller helper)
+
+ class << self
+ def init(initializer)
+ load_extensions
+ Engines::Assets.initialize_base_public_directory
+ end
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+
+ def load_extensions
+ rails_extensions.each { |name| require "engines/rails_extensions/#{name}" }
+ # load the testing extensions, if we are in the test environment.
+ require "engines/testing" if RAILS_ENV == "test"
+ end
+
+ def select_existing_paths(paths)
+ paths.select { |path| File.directory?(path) }
+ end
+
+ # The engines plugin will, by default, mix code from controllers and helpers,
+ # allowing application code to override specific methods in the corresponding
+ # controller or helper classes and modules. However, if other file types should
+ # also be mixed like this, they can be added by calling this method. For example,
+ # if you want to include "things" within your plugin and override them from
+ # your applications, you should use the following layout:
+ #
+ # app/
+ # +-- things/
+ # | +-- one_thing.rb
+ # | +-- another_thing.rb
+ # ...
+ # vendor/
+ # +-- plugins/
+ # +-- my_plugin/
+ # +-- app/
+ # +-- things/
+ # +-- one_thing.rb
+ # +-- another_thing.rb
+ #
+ # The important point here is that your "things" are named <whatever>_thing.rb,
+ # and that they are placed within plugin/app/things (the pluralized form of 'thing').
+ #
+ # It's important to note that you'll also want to ensure that the "things" are
+ # on your load path by including them in Rails load path mechanism, e.g. in init.rb:
+ #
+ # ActiveSupport::Dependencies.load_paths << File.join(File.dirname(__FILE__), 'app', 'things'))
+ #
+ def mix_code_from(*types)
+ self.code_mixing_file_types += types.map { |x| x.to_s.singularize }
+ end
+
+ # A general purpose method to mirror a directory (+source+) into a destination
+ # directory, including all files and subdirectories. Files will not be mirrored
+ # if they are identical already (checked via FileUtils#identical?).
+ def mirror_files_from(source, destination)
+ return unless File.directory?(source)
+
+ # TODO: use Rake::FileList#pathmap?
+ 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, ''))
+ FileUtils.mkdir_p(base_target_dir)
+ 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}: \n" + e
+ 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}: \n" + e
+ end
+ end
+ end
+ end
+end
--- /dev/null
+module Engines
+ module Assets
+ class << self
+ @@readme = %{Files in this directory are automatically generated from your plugins.
+They are copied from the 'assets' directories of each plugin into this directory
+each time Rails starts (script/server, script/console... and so on).
+Any edits you make will NOT persist across the next server restart; instead you
+should edit the files within the <plugin_name>/assets/ directory itself.}
+
+ # Ensure that the plugin asset subdirectory of RAILS_ROOT/public exists, and
+ # that we've added a little warning message to instruct developers not to mess with
+ # the files inside, since they're automatically generated.
+ def initialize_base_public_directory
+ dir = Engines.public_directory
+ unless File.exist?(dir)
+ FileUtils.mkdir_p(dir)
+ end
+ readme = File.join(dir, "README")
+ File.open(readme, 'w') { |f| f.puts @@readme } unless File.exist?(readme)
+ end
+
+ # Replicates the subdirectories under the plugins's +assets+ (or +public+)
+ # directory into the corresponding public directory. See also
+ # Plugin#public_directory for more.
+ def mirror_files_for(plugin)
+ return if plugin.public_directory.nil?
+ begin
+ Engines.mirror_files_from(plugin.public_directory, File.join(Engines.public_directory, plugin.name))
+ rescue Exception => e
+ Engines.logger.warn "WARNING: Couldn't create the public file structure for plugin '#{plugin.name}'; Error follows:"
+ Engines.logger.warn e
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+# An instance of Plugin is created for each plugin loaded by Rails, and
+# stored in the <tt>Engines.plugins</tt> PluginList
+# (see Engines::RailsExtensions::RailsInitializer for more details).
+#
+# Engines.plugins[:plugin_name]
+#
+# Other properties of the Plugin instance can also be set.
+module Engines
+ class Plugin < Rails::Plugin
+ # Plugins can add paths to this attribute in init.rb if they need
+ # controllers loaded from additional locations.
+ attr_accessor :controller_paths
+
+ # The directory in this plugin to mirror into the shared directory
+ # under +public+.
+ #
+ # Defaults to "assets" (see default_public_directory).
+ attr_accessor :public_directory
+
+ protected
+ # The default set of code paths which will be added to the routing system
+ def default_controller_paths
+ %w(app/controllers components)
+ end
+
+ # Attempts to detect the directory to use for public files.
+ # If +assets+ exists in the plugin, this will be used. If +assets+ is missing
+ # but +public+ is found, +public+ will be used.
+ def default_public_directory
+ Engines.select_existing_paths(%w(assets public).map { |p| File.join(directory, p) }).first
+ end
+
+ public
+
+ def initialize(directory)
+ super directory
+ @controller_paths = default_controller_paths
+ @public_directory = default_public_directory
+ end
+
+ # Extends the superclass' load method to additionally mirror public assets
+ def load(initializer)
+ return if loaded?
+ super initializer
+ add_plugin_locale_paths
+ Assets.mirror_files_for(self)
+ end
+
+ # select those paths that actually exist in the plugin's directory
+ def select_existing_paths(name)
+ Engines.select_existing_paths(self.send(name).map { |p| File.join(directory, p) })
+ end
+
+ def add_plugin_locale_paths
+ locale_path = File.join(directory, 'locales')
+ return unless File.exists?(locale_path)
+
+ locale_files = Dir[File.join(locale_path, '*.{rb,yml}')]
+ return if locale_files.blank?
+
+ first_app_element =
+ I18n.load_path.select{ |e| e =~ /^#{ RAILS_ROOT }/ }.reject{ |e| e =~ /^#{ RAILS_ROOT }\/vendor\/plugins/ }.first
+ app_index = I18n.load_path.index(first_app_element) || - 1
+
+ I18n.load_path.insert(app_index, *locale_files)
+ end
+
+ # The path to this plugin's public files
+ def public_asset_directory
+ "#{File.basename(Engines.public_directory)}/#{name}"
+ end
+
+ # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
+ def migration_directory
+ File.join(self.directory, '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. See Engines::Plugin::Migrator for more
+ # information.
+ def migrate(version = nil)
+ Engines::Plugin::Migrator.migrate_plugin(self, version)
+ end
+ end
+end
+
--- /dev/null
+# The PluginList class is an array, enhanced to allow access to loaded plugins
+# by name, and iteration over loaded plugins in order of priority. This array is used
+# by Engines::RailsExtensions::RailsInitializer to create the Engines.plugins array.
+#
+# Each loaded plugin has a corresponding Plugin instance within this array, and
+# the order the plugins were loaded is reflected in the entries in this array.
+#
+# For more information, see the Rails module.
+module Engines
+ class Plugin
+ class List < Array
+ # Finds plugins with the set with the given name (accepts Strings or Symbols), or
+ # index. So, Engines.plugins[0] returns the first-loaded Plugin, and Engines.plugins[:engines]
+ # returns the Plugin instance for the engines plugin itself.
+ def [](name_or_index)
+ if name_or_index.is_a?(Fixnum)
+ super
+ else
+ self.find { |plugin| plugin.name.to_s == name_or_index.to_s }
+ end
+ end
+
+ # Go through each plugin, highest priority first (last loaded first). Effectively,
+ # this is like <tt>Engines.plugins.reverse</tt>
+ def by_precedence
+ reverse
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+module Engines
+ class Plugin
+ class Loader < Rails::Plugin::Loader
+ protected
+ def register_plugin_as_loaded(plugin)
+ super plugin
+ Engines.plugins << plugin
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+module Engines
+ class Plugin
+ class FileSystemLocator < Rails::Plugin::FileSystemLocator
+ def create_plugin(path)
+ plugin = Engines::Plugin.new(path)
+ plugin.valid? ? plugin : nil
+ end
+ end
+ end
+end
+
--- /dev/null
+# The Plugin::Migrator class contains the logic to run migrations from
+# within plugin directories. The directory in which a plugin's migrations
+# should be is determined by the Plugin#migration_directory method.
+#
+# To migrate a plugin, you can simple call the migrate method (Plugin#migrate)
+# with the version number that plugin should be at. The plugin's migrations
+# will then be used to migrate up (or down) to the given version.
+#
+# For more information, see Engines::RailsExtensions::Migrations
+class Engines::Plugin::Migrator < ActiveRecord::Migrator
+
+ # We need to be able to set the 'current' engine 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.name}/) == 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.name}/) == nil }.map(&:to_i).sort
+ end
+
+ def record_version_state_after_migrating(version)
+ super(version.to_s + "-" + current_plugin.name)
+ end
+end
--- /dev/null
+# The engines plugin makes it trivial to share public assets using plugins.
+# To do this, include an <tt>assets</tt> directory within your plugin, and put
+# your javascripts, stylesheets and images in subdirectories of that folder:
+#
+# my_plugin
+# |- init.rb
+# |- lib/
+# |- assets/
+# |- javascripts/
+# | |- my_functions.js
+# |
+# |- stylesheets/
+# | |- my_styles.css
+# |
+# |- images/
+# |- my_face.jpg
+#
+# Files within the <tt>asset</tt> structure are automatically mirrored into
+# a publicly-accessible folder each time your application starts (see
+# Engines::Assets#mirror_assets).
+#
+#
+# == Using plugin assets in views
+#
+# It's also simple to use Rails' helpers in your views to use plugin assets.
+# The default helper methods have been enhanced by the engines plugin to accept
+# a <tt>:plugin</tt> option, indicating the plugin containing the desired asset.
+#
+# For example, it's easy to use plugin assets in your layouts:
+#
+# <%= stylesheet_link_tag "my_styles", :plugin => "my_plugin", :media => "screen" %>
+# <%= javascript_include_tag "my_functions", :plugin => "my_plugin" %>
+#
+# ... and similarly in views and partials, it's easy to use plugin images:
+#
+# <%= image_tag "my_face", :plugin => "my_plugin" %>
+# <!-- or -->
+# <%= image_path "my_face", :plugin => "my_plugin" %>
+#
+# Where the default helpers allow the specification of more than one file (i.e. the
+# javascript and stylesheet helpers), you can do similarly for multiple assets from
+# within a single plugin.
+#
+# ---
+#
+# This module enhances four of the methods from ActionView::Helpers::AssetTagHelper:
+#
+# * stylesheet_link_tag
+# * javascript_include_tag
+# * image_path
+# * image_tag
+#
+# Each one of these methods now accepts the key/value pair <tt>:plugin => "plugin_name"</tt>,
+# which can be used to specify the originating plugin for any assets.
+#
+module Engines::RailsExtensions::AssetHelpers
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ [:stylesheet_link_tag, :javascript_include_tag, :image_path, :image_tag].each do |m|
+ alias_method_chain m, :engine_additions
+ end
+ end
+ end
+
+ # Adds plugin functionality to Rails' default stylesheet_link_tag method.
+ def stylesheet_link_tag_with_engine_additions(*sources)
+ stylesheet_link_tag_without_engine_additions(*Engines::RailsExtensions::AssetHelpers.pluginify_sources("stylesheets", *sources))
+ end
+
+ # Adds plugin functionality to Rails' default javascript_include_tag method.
+ def javascript_include_tag_with_engine_additions(*sources)
+ javascript_include_tag_without_engine_additions(*Engines::RailsExtensions::AssetHelpers.pluginify_sources("javascripts", *sources))
+ end
+
+ #--
+ # Our modified image_path now takes a 'plugin' option, though it doesn't require it
+ #++
+
+ # Adds plugin functionality to Rails' default image_path method.
+ def image_path_with_engine_additions(source, options={})
+ options.stringify_keys!
+ source = Engines::RailsExtensions::AssetHelpers.plugin_asset_path(options["plugin"], "images", source) if options["plugin"]
+ image_path_without_engine_additions(source)
+ end
+
+ # Adds plugin functionality to Rails' default image_tag method.
+ def image_tag_with_engine_additions(source, options={})
+ options.stringify_keys!
+ if options["plugin"]
+ source = Engines::RailsExtensions::AssetHelpers.plugin_asset_path(options["plugin"], "images", source)
+ options.delete("plugin")
+ end
+ image_tag_without_engine_additions(source, options)
+ end
+
+ #--
+ # The following are methods on this module directly because of the weird-freaky way
+ # Rails creates the helper instance that views actually get
+ #++
+
+ # Convert sources to the paths for the given plugin, if any plugin option is given
+ def self.pluginify_sources(type, *sources)
+ options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
+ sources.map! { |s| plugin_asset_path(options["plugin"], type, s) } if options["plugin"]
+ options.delete("plugin") # we don't want it appearing in the HTML
+ sources << options # re-add options
+ end
+
+ # Returns the publicly-addressable relative URI for the given asset, type and plugin
+ def self.plugin_asset_path(plugin_name, type, asset)
+ raise "No plugin called '#{plugin_name}' - please use the full name of a loaded plugin." if Engines.plugins[plugin_name].nil?
+ "/#{Engines.plugins[plugin_name].public_asset_directory}/#{type}/#{asset}"
+ end
+
+end
+
+module ::ActionView::Helpers::AssetTagHelper #:nodoc:
+ include Engines::RailsExtensions::AssetHelpers
+end
\ No newline at end of file
--- /dev/null
+# One of the magic features that that engines plugin provides is the ability to
+# override selected methods in controllers and helpers from your application.
+# This is achieved by trapping requests to load those files, and then mixing in
+# code from plugins (in the order the plugins were loaded) before finally loading
+# any versions from the main +app+ directory.
+#
+# The behaviour of this extension is output to the log file for help when
+# debugging.
+#
+# == Example
+#
+# A plugin contains the following controller in <tt>plugin/app/controllers/my_controller.rb</tt>:
+#
+# class MyController < ApplicationController
+# def index
+# @name = "HAL 9000"
+# end
+# def list
+# @robots = Robot.find(:all)
+# end
+# end
+#
+# In one application that uses this plugin, we decide that the name used in the
+# index action should be "Robbie", not "HAL 9000". To override this single method,
+# we create the corresponding controller in our application
+# (<tt>RAILS_ROOT/app/controllers/my_controller.rb</tt>), and redefine the method:
+#
+# class MyController < ApplicationController
+# def index
+# @name = "Robbie"
+# end
+# end
+#
+# The list method remains as it was defined in the plugin controller.
+#
+# The same basic principle applies to helpers, and also views and partials (although
+# view overriding is performed in Engines::RailsExtensions::Templates; see that
+# module for more information).
+#
+# === What about models?
+#
+# Unfortunately, it's not possible to provide this kind of magic for models.
+# The only reason why it's possible for controllers and helpers is because
+# they can be recognised by their filenames ("whatever_controller", "jazz_helper"),
+# whereas models appear the same as any other typical Ruby library ("node",
+# "user", "image", etc.).
+#
+# If mixing were allowed in models, it would mean code mixing for *every*
+# file that was loaded via +require_or_load+, and this could result in
+# problems where, for example, a Node model might start to include
+# functionality from another file called "node" somewhere else in the
+# <tt>$LOAD_PATH</tt>.
+#
+# One way to overcome this is to provide model functionality as a module in
+# a plugin, which developers can then include into their own model
+# implementations.
+#
+# Another option is to provide an abstract model (see the ActiveRecord::Base
+# documentation) and have developers subclass this model in their own
+# application if they must.
+#
+# ---
+#
+# The Engines::RailsExtensions::Dependencies module includes a method to
+# override Dependencies.require_or_load, which is called to load code needed
+# by Rails as it encounters constants that aren't defined.
+#
+# This method is enhanced with the code-mixing features described above.
+#
+module Engines::RailsExtensions::Dependencies
+ def self.included(base) #:nodoc:
+ base.class_eval { alias_method_chain :require_or_load, :engine_additions }
+ end
+
+ # Attempt to load the given file from any plugins, as well as the application.
+ # This performs the 'code mixing' magic, allowing application controllers and
+ # helpers to override single methods from those in plugins.
+ # If the file can be found in any plugins, it will be loaded first from those
+ # locations. Finally, the application version is loaded, using Ruby's behaviour
+ # to replace existing methods with their new definitions.
+ #
+ # If <tt>Engines.disable_code_mixing == true</tt>, the first controller/helper on the
+ # <tt>$LOAD_PATH</tt> will be used (plugins' +app+ directories are always lower on the
+ # <tt>$LOAD_PATH</tt> than the main +app+ directory).
+ #
+ # If <tt>Engines.disable_application_code_loading == true</tt>, controllers will
+ # not be loaded from the main +app+ directory *if* they are present in any
+ # plugins.
+ #
+ # Returns true if the file could be loaded (from anywhere); false otherwise -
+ # mirroring the behaviour of +require_or_load+ from Rails (which mirrors
+ # that of Ruby's own +require+, I believe).
+ def require_or_load_with_engine_additions(file_name, const_path=nil)
+ return require_or_load_without_engine_additions(file_name, const_path) if Engines.disable_code_mixing
+
+ file_loaded = false
+
+ # try and load the plugin code first
+ # can't use model, as there's nothing in the name to indicate that the file is a 'model' file
+ # rather than a library or anything else.
+ Engines.code_mixing_file_types.each do |file_type|
+ # if we recognise this type
+ # (this regexp splits out the module/filename from any instances of app/#{type}, so that
+ # modules are still respected.)
+ if file_name =~ /^(.*app\/#{file_type}s\/)+(.*_#{file_type})(\.rb)?$/
+ base_name = $2
+ # ... go through the plugins from first started to last, so that
+ # code with a high precedence (started later) will override lower precedence
+ # implementations
+ Engines.plugins.each do |plugin|
+ plugin_file_name = File.expand_path(File.join(plugin.directory, 'app', "#{file_type}s", base_name))
+ if File.file?("#{plugin_file_name}.rb")
+ file_loaded = true if require_or_load_without_engine_additions(plugin_file_name, const_path)
+ end
+ end
+
+ # finally, load any application-specific controller classes using the 'proper'
+ # rails load mechanism, EXCEPT when we're testing engines and could load this file
+ # from an engine
+ unless Engines.disable_application_code_loading
+ # Ensure we are only loading from the /app directory at this point
+ app_file_name = File.join(RAILS_ROOT, 'app', "#{file_type}s", "#{base_name}")
+ if File.file?("#{app_file_name}.rb")
+ file_loaded = true if require_or_load_without_engine_additions(app_file_name, const_path)
+ end
+ end
+ end
+ end
+
+ # if we managed to load a file, return true. If not, default to the original method.
+ # Note that this relies on the RHS of a boolean || not to be evaluated if the LHS is true.
+ file_loaded || require_or_load_without_engine_additions(file_name, const_path)
+ end
+end
+
+module ActiveSupport::Dependencies #:nodoc:
+ include Engines::RailsExtensions::Dependencies
+end
--- /dev/null
+# == Using plugin assets for form tag helpers
+#
+# It's as easy to use plugin images for image_submit_tag using Engines as it is for image_tag:
+#
+# <%= image_submit_tag "my_face", :plugin => "my_plugin" %>
+#
+# ---
+#
+# This module enhances one of the methods from ActionView::Helpers::FormTagHelper:
+#
+# * image_submit_tag
+#
+# This method now accepts the key/value pair <tt>:plugin => "plugin_name"</tt>,
+# which can be used to specify the originating plugin for any assets.
+#
+module Engines::RailsExtensions::FormTagHelpers
+ def self.included(base)
+ base.class_eval do
+ alias_method_chain :image_submit_tag, :engine_additions
+ end
+ end
+
+ # Adds plugin functionality to Rails' default image_submit_tag method.
+ def image_submit_tag_with_engine_additions(source, options={})
+ options.stringify_keys!
+ if options["plugin"]
+ source = Engines::RailsExtensions::AssetHelpers.plugin_asset_path(options["plugin"], "images", source)
+ options.delete("plugin")
+ end
+ image_submit_tag_without_engine_additions(source, options)
+ end
+end
+
+module ::ActionView::Helpers::FormTagHelper #:nodoc:
+ include Engines::RailsExtensions::FormTagHelpers
+end
+
--- /dev/null
+# Contains the enhancements to Rails' migrations system to support the
+# Engines::Plugin::Migrator. See Engines::RailsExtensions::Migrations for more
+# information.
+
+require "engines/plugin/migrator"
+
+# = Plugins and Migrations: Background
+#
+# Rails uses migrations to describe changes to the databases as your application
+# evolves. Each change to your application - adding and removing models, most
+# commonly - might require tweaks to your schema in the form of new tables, or new
+# columns on existing tables, or possibly the removal of tables or columns. Migrations
+# can even include arbitrary code to *transform* data as the underlying schema
+# changes.
+#
+# The point is that at any particular stage in your application's development,
+# migrations serve to transform the database into a state where it is compatible
+# and appropriate at that time.
+#
+# == What about plugins?
+#
+# If you want to share models using plugins, chances are that you might also
+# want to include the corresponding migrations to create tables for those models.
+# With the engines plugin installed, plugins can carry migration data easily:
+#
+# vendor/
+# |
+# plugins/
+# |
+# my_plugin/
+# |- init.rb
+# |- lib/
+# |- db/
+# |-migrate/
+# |- 20081105123419_add_some_new_feature.rb
+# |- 20081107144959_and_something_else.rb
+# |- ...
+#
+# When you install a plugin which contains migrations, you are undertaking a
+# further step in the development of your application, the same as the addition
+# of any other code. With this in mind, you may want to 'roll back' the
+# installation of this plugin at some point, and the database should be able
+# to migrate back to the point without this plugin in it too.
+#
+# == An example
+#
+# For example, our current application is at version 20081106164503 (according to the
+# +schema_migrations+ table), when we decide that we want to add a tagging plugin. The
+# tagging plugin chosen includes migrations to create the tables it requires
+# (say, _tags_ and _taggings_, for instance), along with the models and helpers
+# one might expect.
+#
+# After installing this plugin, these tables should be created in our database.
+# Rather than running the migrations directly from the plugin, they should be
+# integrated into our main migration stream in order to accurately reflect the
+# state of our application's database *at this moment in time*.
+#
+# $ script/generate plugin_migration
+# exists db/migrate
+# create db/migrate/20081108120415_my_plugin_to_version_20081107144959.rb
+#
+# This migration will take our application to version 20081108120415, and contains the
+# following, typical migration code:
+#
+# class TaggingToVersion20081107144959 < ActiveRecord::Migration
+# def self.up
+# Engines.plugins[:tagging].migrate(20081107144959)
+# end
+# def self.down
+# Engines.plugins[:tagging].migrate(0)
+# end
+# end
+#
+# When we migrate our application up, using <tt>rake db:migrate</tt> as normal,
+# the plugin will be migrated up to its latest version (20081108120415 in this example). If we
+# ever decide to migrate the application back to the state it was in at version 20081106164503,
+# the plugin migrations will be taken back down to version 0 (which, typically,
+# would remove all tables the plugin migrations define).
+#
+# == Upgrading plugins
+#
+# It might happen that later in an application's life, we update to a new version of
+# the tagging plugin which requires some changes to our database. The tagging plugin
+# provides these changes in the form of its own migrations.
+#
+# In this case, we just need to re-run the plugin_migration generator to create a
+# new migration from the current revision to the newest one:
+#
+# $ script/generate plugin_migration
+# exists db/migrate
+# create db/migrate/20081210131437_tagging_to_version_20081201172034.rb
+#
+# The contents of this migration are:
+#
+# class TaggingToVersion20081108120415 < ActiveRecord::Migration
+# def self.up
+# Engines.plugins[:tagging].migrate(20081201172034)
+# end
+# def self.down
+# Engines.plugins[:tagging].migrate(20081107144959)
+# end
+# end
+#
+# Notice that if we were to migrate down to revision 20081108120415 or lower, the tagging plugin
+# will be migrated back down to version 20081107144959 - the version we were previously at.
+#
+#
+# = Creating migrations in plugins
+#
+# In order to use the plugin migration functionality that engines provides, a plugin
+# only needs to provide regular migrations in a <tt>db/migrate</tt> folder within it.
+#
+# = Explicitly migrating plugins
+#
+# It's possible to migrate plugins within your own migrations, or any other code.
+# Simply get the Plugin instance, and its Plugin#migrate method with the version
+# you wish to end up at:
+#
+# Engines.plugins[:whatever].migrate(version)
+#
+#
+# = Upgrading from previous versions of the engines plugin
+#
+# Thanks to the tireless work of the plugin developer community, we can now relying on the migration
+# mechanism in Rails 2.1+ to do much of the plugin migration work for us. This also means that we
+# don't need a seperate schema_info table for plugins.
+#
+# To update your application, run
+#
+# rake db:migrate:upgrade_plugin_migrations
+#
+# This will ensure that migration information is carried over into the main schema_migrations table.
+#
\ No newline at end of file
--- /dev/null
+# This is only here to allow for backwards compability with Engines that
+# have been implemented based on Engines for Rails 1.2. It is preferred that
+# the plugin list be accessed via Engines.plugins.
+
+module Rails
+ # Returns the Engines::Plugin::List from Engines.plugins. It is preferable to
+ # access Engines.plugins directly.
+ def self.plugins
+ Engines.plugins
+ end
+end
--- /dev/null
+# Contains the enhancements to assist in testing plugins. See Engines::Testing
+# for more details.
+
+require 'test/unit'
+
+require 'tmpdir'
+require 'fileutils'
+
+# In most cases, Rails' own plugin testing mechanisms are sufficient. However, there
+# are cases where plugins can be given a helping hand in the testing arena. This module
+# contains some methods to assist when testing plugins that contain fixtures.
+#
+# == Fixtures and plugins
+#
+# Since Rails' own fixtures method is fairly strict about where files can be loaded from,
+# the simplest approach when running plugin tests with fixtures is to simply copy all
+# fixtures into a single temporary location and inform the standard Rails mechanism to
+# use this directory, rather than RAILS_ROOT/test/fixtures.
+#
+# The Engines::Testing#setup_plugin_fixtures method does this, copying all plugin fixtures
+# into the temporary location before and tests are performed. This behaviour is invoked
+# the the rake tasks provided by the Engines plugin, in the "test:plugins" namespace. If
+# necessary, you can invoke the task manually.
+#
+# If you wish to take advantage of this, add a call to the Engines::Testing.set_fixture_path
+# method somewhere before your tests (in a test_helper file, or above the TestCase itself).
+#
+# = Testing plugins
+#
+# Normally testing a plugin will require that Rails is loaded, unless you are including
+# a skeleton Rails environment or set of mocks within your plugin tests. If you require
+# the Rails environment to be started, you must ensure that this actually happens; while
+# it's not obvious, your tests do not automatically run with Rails loaded.
+#
+# The simplest way to setup plugin tests is to include a test helper with the following
+# contents:
+#
+# # Load the normal Rails helper. This ensures the environment is loaded
+# require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
+# # Ensure that we are using the temporary fixture path
+# Engines::Testing.set_fixture_path
+#
+# Then run tests using the provided tasks (<tt>test:plugins</tt>, or the tasks that the engines
+# plugin provides - <tt>test:plugins:units</tt>, etc.).
+#
+# Alternatively, you can explicitly load the environment by adpating the contents of the
+# default <tt>test_helper</tt>:
+#
+# ENV["RAILS_ENV"] = "test"
+# # Note that we are requiring config/environment from the root of the enclosing application.
+# require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
+# require 'test_help'
+#
+module Engines::Testing
+ mattr_accessor :temporary_fixtures_directory
+ self.temporary_fixtures_directory = FileUtils.mkdir_p(File.join(Dir.tmpdir, "rails_fixtures"))
+
+ # Copies fixtures from plugins and the application into a temporary directory
+ # (Engines::Testing.temporary_fixtures_directory).
+ #
+ # If a set of plugins is not given, fixtures are copied from all plugins in order
+ # of precedence, meaning that plugins can 'overwrite' the fixtures of others if they are
+ # loaded later; the application's fixtures are copied last, allowing any custom fixtures
+ # to override those in the plugins. If no argument is given, plugins are loaded via
+ # PluginList#by_precedence.
+ #
+ # This method is called by the engines-supplied plugin testing rake tasks
+ def self.setup_plugin_fixtures(plugins = Engines.plugins.by_precedence)
+
+ # First, clear the directory
+ Dir.glob("#{self.temporary_fixtures_directory}/*.yml").each{|fixture| File.delete(fixture)}
+
+ # Copy all plugin fixtures, and then the application fixtures, into this directory
+ plugins.each do |plugin|
+ plugin_fixtures_directory = File.join(plugin.directory, "test", "fixtures")
+ plugin_app_directory = File.join(plugin.directory, "app")
+ if File.directory?(plugin_app_directory) && File.directory?(plugin_fixtures_directory)
+ Engines.mirror_files_from(plugin_fixtures_directory, self.temporary_fixtures_directory)
+ end
+ end
+ Engines.mirror_files_from(File.join(RAILS_ROOT, "test", "fixtures"),
+ self.temporary_fixtures_directory)
+ end
+
+ # Sets the fixture path used by Test::Unit::TestCase to the temporary
+ # directory which contains all plugin fixtures.
+ def self.set_fixture_path
+ ActiveSupport::TestCase.fixture_path = self.temporary_fixtures_directory
+ $LOAD_PATH.unshift self.temporary_fixtures_directory
+ end
+
+ # overridden test should be in test/{unit,functional,integration}/{plugin_name}/{test_name}
+ def self.override_tests_from_app
+ filename = caller.first.split(":").first
+ plugin_name = filename.split("/")[-4]
+ test_kind = filename.split("/")[-2]
+ override_file = File.expand_path(File.join(File.dirname(filename), "..", "..", "..", "..", "..", "test",
+ test_kind, plugin_name, File.basename(filename)))
+ load(override_file) if File.exist?(override_file)
+ end
+end
\ No newline at end of file
--- /dev/null
+# This code lets us redefine existing Rake tasks, which is extremely
+# handy for modifying existing Rails rake tasks.
+# Credit for the original snippet of code goes to Jeremy Kemper
+# http://pastie.caboo.se/9620
+unless Rake::TaskManager.methods.include?('redefine_task')
+ module Rake
+ module TaskManager
+ def redefine_task(task_class, args, &block)
+ task_name, arg_names, deps = resolve_args([args])
+ task_name = task_class.scope_name(@scope, task_name)
+ deps = [deps] unless deps.respond_to?(:to_ary)
+ deps = deps.collect {|d| d.to_s }
+ task = @tasks[task_name.to_s] = task_class.new(task_name, self)
+ task.application = self
+ task.add_description(@last_description)
+ @last_description = nil
+ task.enhance(deps, &block)
+ task
+ end
+
+ end
+ class Task
+ class << self
+ def redefine_task(args, &block)
+ Rake.application.redefine_task(self, [args], &block)
+ end
+ end
+ end
+ end
+end
+
+namespace :db do
+ namespace :migrate do
+ desc 'Migrate database and plugins to current status.'
+ task :all => [ 'db:migrate', 'db:migrate:plugins' ]
+
+ desc 'Migrate plugins to current status.'
+ task :plugins => :environment do
+ Engines.plugins.each do |plugin|
+ next unless File.exists? plugin.migration_directory
+ puts "Migrating plugin #{plugin.name} ..."
+ plugin.migrate
+ end
+ end
+
+ desc 'For engines coming from Rails version < 2.0 or for those previously updated to work with Sven Fuch\'s fork of engines, you need to upgrade the schema info table'
+ task :upgrade_plugin_migrations => :environment do
+ svens_fork_table_name = 'plugin_schema_migrations'
+
+ # Check if app was previously using Sven's fork
+ if ActiveRecord::Base.connection.table_exists?(svens_fork_table_name)
+ old_sm_table = svens_fork_table_name
+ else
+ old_sm_table = ActiveRecord::Migrator.proper_table_name(Engines.schema_info_table)
+ end
+
+ unless ActiveRecord::Base.connection.table_exists?(old_sm_table)
+ abort "Cannot find old migration table - assuming nothing needs to be done"
+ end
+
+ # There are two forms of the engines schema info - pre-fix_plugin_migrations and post
+ # We need to figure this out before we continue.
+
+ results = ActiveRecord::Base.connection.select_rows(
+ "SELECT version, plugin_name FROM #{old_sm_table}"
+ ).uniq
+
+ def insert_new_version(plugin_name, version)
+ version_string = "#{version}-#{plugin_name}"
+ new_sm_table = ActiveRecord::Migrator.schema_migrations_table_name
+
+ # Check if the row already exists for some reason - maybe run this task more than once.
+ return if ActiveRecord::Base.connection.select_rows("SELECT * FROM #{new_sm_table} WHERE version = #{version_string.dump.gsub("\"", "'")}").size > 0
+
+ puts "Inserting new version #{version} for plugin #{plugin_name}.."
+ ActiveRecord::Base.connection.insert("INSERT INTO #{new_sm_table} (version) VALUES (#{version_string.dump.gsub("\"", "'")})")
+ end
+
+ # We need to figure out if they already used "fix_plugin_migrations"
+ versions = {}
+ results.each do |r|
+ versions[r[1]] ||= []
+ versions[r[1]] << r[0].to_i
+ end
+
+ if versions.values.find{ |v| v.size > 1 } == nil
+ puts "Fixing migration info"
+ # We only have one listed migration per plugin - this is pre-fix_plugin_migrations,
+ # so we build all versions required. In this case, all migrations should
+ versions.each do |plugin_name, version|
+ version = version[0] # There is only one version
+
+ # We have to make an assumption that numeric migrations won't get this long..
+ # I'm not sure if there is a better assumption, it should work in all
+ # current cases.. (touch wood..)
+ if version.to_s.size < "YYYYMMDDHHMMSS".size
+ # Insert version records for each migration
+ (1..version).each do |v|
+ insert_new_version(plugin_name, v)
+ end
+ else
+ # If the plugin is new-format "YYYYMMDDHHMMSS", we just copy it across...
+ # The case in which this occurs is very rare..
+ insert_new_version(plugin_name, version)
+ end
+ end
+ else
+ puts "Moving migration info"
+ # We have multiple migrations listed per plugin - thus we can assume they have
+ # already applied fix_plugin_migrations - we just copy it across verbatim
+ versions.each do |plugin_name, version|
+ version.each { |v| insert_new_version(plugin_name, v) }
+ end
+ end
+
+ puts "Migration info successfully migrated - removing old schema info table"
+ ActiveRecord::Base.connection.drop_table(old_sm_table)
+ end
+
+ desc 'Migrate a specified plugin.'
+ task(:plugin => :environment) do
+ name = ENV['NAME']
+ if plugin = Engines.plugins[name]
+ version = ENV['VERSION']
+ puts "Migrating #{plugin.name} to " + (version ? "version #{version}" : 'latest version') + " ..."
+ plugin.migrate(version ? version.to_i : nil)
+ else
+ puts "Plugin #{name} does not exist."
+ end
+ end
+ end
+end
+
+
+namespace :db do
+ namespace :fixtures do
+ namespace :plugins do
+
+ desc "Load plugin fixtures into the current environment's database."
+ task :load => :environment do
+ require 'active_record/fixtures'
+ ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
+ Dir.glob(File.join(RAILS_ROOT, 'vendor', 'plugins', ENV['PLUGIN'] || '**',
+ 'test', 'fixtures', '*.yml')).each do |fixture_file|
+ Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*'))
+ end
+ end
+
+ end
+ end
+end
+
+# this is just a modification of the original task in railties/lib/tasks/documentation.rake,
+# because the default task doesn't support subdirectories like <plugin>/app or
+# <plugin>/component. These tasks now include every file under a plugin's load paths (see
+# Plugin#load_paths).
+namespace :doc do
+
+ plugins = FileList['vendor/plugins/**'].collect { |plugin| File.basename(plugin) }
+
+ namespace :plugins do
+
+ # Define doc tasks for each plugin
+ plugins.each do |plugin|
+ desc "Create plugin documentation for '#{plugin}'"
+ Rake::Task.redefine_task(plugin => :environment) do
+ plugin_base = RAILS_ROOT + "/vendor/plugins/#{plugin}"
+ options = []
+ files = Rake::FileList.new
+ options << "-o doc/plugins/#{plugin}"
+ options << "--title '#{plugin.titlecase} Plugin Documentation'"
+ options << '--line-numbers' << '--inline-source'
+ options << '-T html'
+
+ # Include every file in the plugin's load_paths (see Plugin#load_paths)
+ if Engines.plugins[plugin]
+ files.include("#{plugin_base}/{#{Engines.plugins[plugin].load_paths.join(",")}}/**/*.rb")
+ end
+ if File.exists?("#{plugin_base}/README")
+ files.include("#{plugin_base}/README")
+ options << "--main '#{plugin_base}/README'"
+ end
+ files.include("#{plugin_base}/CHANGELOG") if File.exists?("#{plugin_base}/CHANGELOG")
+
+ if files.empty?
+ puts "No source files found in #{plugin_base}. No documentation will be generated."
+ else
+ options << files.to_s
+ sh %(rdoc #{options * ' '})
+ end
+ end
+ end
+ end
+end
+
+
+
+namespace :test do
+ task :warn_about_multiple_plugin_testing_with_engines do
+ puts %{-~============== A Moste Polite Warninge ===========================~-
+
+You may experience issues testing multiple plugins at once when using
+the code-mixing features that the engines plugin provides. If you do
+experience any problems, please test plugins individually, i.e.
+
+ $ rake test:plugins PLUGIN=my_plugin
+
+or use the per-type plugin test tasks:
+
+ $ rake test:plugins:units
+ $ rake test:plugins:functionals
+ $ rake test:plugins:integration
+ $ rake test:plugins:all
+
+Report any issues on http://dev.rails-engines.org. Thanks!
+
+-~===============( ... as you were ... )============================~-}
+ end
+
+ namespace :engines do
+
+ def engine_plugins
+ Dir["vendor/plugins/*"].select { |f| File.directory?(File.join(f, "app")) }.map { |f| File.basename(f) }.join(",")
+ end
+
+ desc "Run tests from within engines plugins (plugins with an 'app' directory)"
+ task :all => [:units, :functionals, :integration]
+
+ desc "Run unit tests from within engines plugins (plugins with an 'app' directory)"
+ Rake::TestTask.new(:units => "test:plugins:setup_plugin_fixtures") do |t|
+ t.pattern = "vendor/plugins/{#{ENV['PLUGIN'] || engine_plugins}}/test/unit/**/*_test.rb"
+ t.verbose = true
+ end
+
+ desc "Run functional tests from within engines plugins (plugins with an 'app' directory)"
+ Rake::TestTask.new(:functionals => "test:plugins:setup_plugin_fixtures") do |t|
+ t.pattern = "vendor/plugins/{#{ENV['PLUGIN'] || engine_plugins}}/test/functional/**/*_test.rb"
+ t.verbose = true
+ end
+
+ desc "Run integration tests from within engines plugins (plugins with an 'app' directory)"
+ Rake::TestTask.new(:integration => "test:plugins:setup_plugin_fixtures") do |t|
+ t.pattern = "vendor/plugins/{#{ENV['PLUGIN'] || engine_plugins}}/test/integration/**/*_test.rb"
+ t.verbose = true
+ end
+ end
+
+ namespace :plugins do
+
+ desc "Run the plugin tests in vendor/plugins/**/test (or specify with PLUGIN=name)"
+ task :all => [:warn_about_multiple_plugin_testing_with_engines,
+ :units, :functionals, :integration]
+
+ desc "Run all plugin unit tests"
+ Rake::TestTask.new(:units => :setup_plugin_fixtures) do |t|
+ t.pattern = "vendor/plugins/#{ENV['PLUGIN'] || "**"}/test/unit/**/*_test.rb"
+ t.verbose = true
+ end
+
+ desc "Run all plugin functional tests"
+ Rake::TestTask.new(:functionals => :setup_plugin_fixtures) do |t|
+ t.pattern = "vendor/plugins/#{ENV['PLUGIN'] || "**"}/test/functional/**/*_test.rb"
+ t.verbose = true
+ end
+
+ desc "Integration test engines"
+ Rake::TestTask.new(:integration => :setup_plugin_fixtures) do |t|
+ t.pattern = "vendor/plugins/#{ENV['PLUGIN'] || "**"}/test/integration/**/*_test.rb"
+ t.verbose = true
+ end
+
+ desc "Mirrors plugin fixtures into a single location to help plugin tests"
+ task :setup_plugin_fixtures => :environment do
+ Engines::Testing.setup_plugin_fixtures
+ end
+
+ # Patch the default plugin testing task to have setup_plugin_fixtures as a prerequisite
+ Rake::Task["test:plugins"].prerequisites << "test:plugins:setup_plugin_fixtures"
+ end
+end
--- /dev/null
+class AppAndPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from app'
+ end
+end
--- /dev/null
+class Namespace::AppAndPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from app'
+ end
+end
--- /dev/null
+module MailHelper
+ def do_something_helpful(var)
+ var.to_s.reverse
+ end
+end
\ No newline at end of file
--- /dev/null
+class AppAndPluginModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+class NotifyMail < ActionMailer::Base
+
+ helper :mail
+
+ def signup(txt)
+ body(:name => txt)
+ end
+
+ def multipart
+ recipients 'some_address@email.com'
+ subject 'multi part email'
+ from "another_user@email.com"
+ content_type 'multipart/alternative'
+
+ part :content_type => "text/html", :body => render_message("multipart_html", {})
+ part "text/plain" do |p|
+ p.body = render_message("multipart_plain", {})
+ end
+ end
+
+ def implicit_multipart
+ recipients 'some_address@email.com'
+ subject 'multi part email'
+ from "another_user@email.com"
+ end
+end
\ No newline at end of file
--- /dev/null
+class Thing
+ def self.from_app; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from app)
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from app)
\ No newline at end of file
--- /dev/null
+the implicit html part of the email <%= do_something_helpful("semaj") %>
\ No newline at end of file
--- /dev/null
+the implicit plaintext part of the email
\ No newline at end of file
--- /dev/null
+the html part of the email <%= do_something_helpful("semaj") %>
\ No newline at end of file
--- /dev/null
+the plaintext part of the email
\ No newline at end of file
--- /dev/null
+Signup template from application
+
+Here's a local variable set in the Mail object: <%= @name %>.
+
+And here's a method called in a mail helper: <%= do_something_helpful(@name) %>
--- /dev/null
+<%= @note %> (from application)
\ No newline at end of file
--- /dev/null
+plugin mail template loaded from application
\ No newline at end of file
--- /dev/null
+# Tests in this file ensure that:
+#
+# * plugin controller actions are found
+# * actions defined in application controllers take precedence over those in plugins
+# * actions in controllers in subsequently loaded plugins take precendence over those in previously loaded plugins
+# * this works for actions in namespaced controllers accordingly
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ControllerLoadingTest < ActionController::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # plugin controller actions should be found
+
+ def test_WITH_an_action_defined_only_in_a_plugin_IT_should_use_this_action
+ get_action_on_controller :an_action, :alpha_plugin
+ assert_response_body 'rendered in AlphaPluginController#an_action'
+ end
+
+ def test_WITH_an_action_defined_only_in_a_namespaced_plugin_controller_IT_should_use_this_action
+ get_action_on_controller :an_action, :alpha_plugin, :namespace
+ assert_response_body 'rendered in Namespace::AlphaPluginController#an_action'
+ end
+
+ # app takes precedence over plugins
+
+ def test_WITH_an_action_defined_in_both_app_and_plugin_IT_should_use_the_one_in_app
+ get_action_on_controller :an_action, :app_and_plugin
+ assert_response_body 'rendered in AppAndPluginController#an_action (from app)'
+ end
+
+ def test_WITH_an_action_defined_in_namespaced_controllers_in_both_app_and_plugin_IT_should_use_the_one_in_app
+ get_action_on_controller :an_action, :app_and_plugin, :namespace
+ assert_response_body 'rendered in Namespace::AppAndPluginController#an_action (from app)'
+ end
+
+ # subsequently loaded plugins take precendence over previously loaded plugins
+
+ def test_WITH_an_action_defined_in_two_plugin_controllers_IT_should_use_the_latter_of_both
+ get_action_on_controller :an_action, :shared_plugin
+ assert_response_body 'rendered in SharedPluginController#an_action (from beta_plugin)'
+ end
+
+ def test_WITH_an_action_defined_in_two_namespaced_plugin_controllers_IT_should_use_the_latter_of_both
+ get_action_on_controller :an_action, :shared_plugin, :namespace
+ assert_response_body 'rendered in Namespace::SharedPluginController#an_action (from beta_plugin)'
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ExceptionNotificationCompatibilityTest < ActionController::TestCase
+ ExceptionNotifier.exception_recipients = %w(joe@schmoe.com bill@schmoe.com)
+ class SimpleController < ApplicationController
+ include ExceptionNotifiable
+ local_addresses.clear
+ consider_all_requests_local = false
+ def index
+ begin
+ raise "Fail!"
+ rescue Exception => e
+ rescue_action_in_public(e)
+ end
+ end
+ end
+
+ def setup
+ @controller = SimpleController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_work
+ assert_nothing_raised do
+ get :index
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+# Tests in this file ensure that:
+#
+# * translations in the application take precedence over those in plugins
+# * translations in subsequently loaded plugins take precendence over those in previously loaded plugins
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class LocaleLoadingTest < ActionController::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # app takes precedence over plugins
+
+ def test_WITH_a_translation_defined_in_both_app_and_plugin_IT_should_find_the_one_in_app
+ assert_equal I18n.t('hello'), 'Hello world'
+ end
+
+ # subsequently loaded plugins take precendence over previously loaded plugins
+
+ def test_WITH_a_translation_defined_in_two_plugins_IT_should_find_the_latter_of_both
+ assert_equal I18n.t('plugin'), 'beta'
+ end
+end
+
--- /dev/null
+# Tests in this file ensure that:
+#
+# * Routes from plugins can be routed to
+# * Named routes can be defined within a plugin
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RoutesTest < ActionController::TestCase
+ tests TestRoutingController
+
+ def test_WITH_a_route_defined_in_a_plugin_IT_should_route_it
+ path = '/routes/an_action'
+ opts = {:controller => 'test_routing', :action => 'an_action'}
+ assert_routing path, opts
+ assert_recognizes opts, path # not sure what exactly the difference is, but it won't hurt either
+ end
+
+ def test_WITH_a_route_for_a_namespaced_controller_defined_in_a_plugin_IT_should_route_it
+ path = 'somespace/routes/an_action'
+ opts = {:controller => 'namespace/test_routing', :action => 'an_action'}
+ assert_routing path, opts
+ assert_recognizes opts, path
+ end
+
+ def test_should_properly_generate_named_routes
+ get :test_named_routes_from_plugin
+ assert_response_body '/somespace/routes'
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ViewHelpersTest < ActionController::TestCase
+ tests AssetsController
+
+ def setup
+ get :index
+ end
+
+ def test_plugin_javascript_helpers
+ base_selector = "script[type='text/javascript']"
+ js_dir = "/plugin_assets/test_assets/javascripts"
+ assert_select "#{base_selector}[src='#{js_dir}/file.1.js']"
+ assert_select "#{base_selector}[src='#{js_dir}/file2.js']"
+ end
+
+ def test_plugin_stylesheet_helpers
+ base_selector = "link[media='screen'][rel='stylesheet'][type='text/css']"
+ css_dir = "/plugin_assets/test_assets/stylesheets"
+ assert_select "#{base_selector}[href='#{css_dir}/file.1.css']"
+ assert_select "#{base_selector}[href='#{css_dir}/file2.css']"
+ end
+
+ def test_plugin_image_helpers
+ assert_select "img[src='/plugin_assets/test_assets/images/image.png'][alt='Image']"
+ end
+
+ def test_plugin_layouts
+ get :index
+ assert_select "div[id='assets_layout']"
+ end
+
+ def test_plugin_image_submit_helpers
+ assert_select "input[src='/plugin_assets/test_assets/images/image.png'][type='image']"
+ end
+
+end
--- /dev/null
+# Tests in this file ensure that:
+#
+# * plugin views are found
+# * views in the application take precedence over those in plugins
+# * views in subsequently loaded plugins take precendence over those in previously loaded plugins
+# * this works for namespaced views accordingly
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ViewLoadingTest < ActionController::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # plugin views should be found
+
+ def test_WITH_a_view_defined_only_in_a_plugin_IT_should_find_the_view
+ get_action_on_controller :a_view, :alpha_plugin
+ assert_response_body 'alpha_plugin/a_view'
+ end
+
+ def test_WITH_a_namespaced_view_defined_only_in_a_plugin_IT_should_find_the_view
+ get_action_on_controller :a_view, :alpha_plugin, :namespace
+ assert_response_body 'namespace/alpha_plugin/a_view'
+ end
+
+ # app takes precedence over plugins
+
+ def test_WITH_a_view_defined_in_both_app_and_plugin_IT_should_find_the_one_in_app
+ get_action_on_controller :a_view, :app_and_plugin
+ assert_response_body 'app_and_plugin/a_view (from app)'
+ end
+
+ def test_WITH_a_namespaced_view_defined_in_both_app_and_plugin_IT_should_find_the_one_in_app
+ get_action_on_controller :a_view, :app_and_plugin, :namespace
+ assert_response_body 'namespace/app_and_plugin/a_view (from app)'
+ end
+
+ # subsequently loaded plugins take precendence over previously loaded plugins
+
+ def test_WITH_a_view_defined_in_two_plugins_IT_should_find_the_latter_of_both
+ get_action_on_controller :a_view, :shared_plugin
+ assert_response_body 'shared_plugin/a_view (from beta_plugin)'
+ end
+
+ def test_WITH_a_namespaced_view_defined_in_two_plugins_IT_should_find_the_latter_of_both
+ get_action_on_controller :a_view, :shared_plugin, :namespace
+ assert_response_body 'namespace/shared_plugin/a_view (from beta_plugin)'
+ end
+
+ # layouts loaded from plugins
+
+ def test_should_be_able_to_load_a_layout_from_a_plugin
+ get_action_on_controller :action_with_layout, :alpha_plugin
+ assert_response_body 'rendered in AlphaPluginController#action_with_layout (with plugin layout)'
+ end
+
+end
+
\ No newline at end of file
--- /dev/null
+class AppAndPluginLibModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+module TestHelper
+ def self.report_location(path)
+ [RAILS_ROOT + '/', 'vendor/plugins/'].each { |part| path.sub! part, ''}
+ path = path.split('/')
+ location, subject = path.first, path.last
+ if subject.sub! '.rb', ''
+ subject = subject.classify
+ else
+ subject.sub! '.html.erb', ''
+ end
+ "#{subject} (from #{location})"
+ end
+
+ def self.view_path_for path
+ [RAILS_ROOT + '/', 'vendor/plugins/', '.html.erb'].each { |part| path.sub! part, ''}
+ parts = path.split('/')
+ parts[(parts.index('views')+1)..-1].join('/')
+ end
+end
+
+class Test::Unit::TestCase
+ # Add more helper methods to be used by all tests here...
+ def get_action_on_controller(*args)
+ action = args.shift
+ with_controller *args
+ get action
+ end
+
+ def with_controller(controller, namespace = nil)
+ classname = controller.to_s.classify + 'Controller'
+ classname = namespace.to_s.classify + '::' + classname unless namespace.nil?
+ @controller = classname.constantize.new
+ end
+
+ def assert_response_body(expected)
+ assert_equal expected, @response.body
+ end
+end
+
+# Because we're testing this behaviour, we actually want these features on!
+Engines.disable_application_view_loading = false
+Engines.disable_application_code_loading = false
--- /dev/null
+module RenderInformation
+ def render_class_and_action(note = nil, options={})
+ text = "rendered in #{self.class.name}##{params[:action]}"
+ text += " (#{note})" unless note.nil?
+ render options.update(:text => text)
+ end
+end
\ No newline at end of file
--- /dev/null
+class AlphaPluginController < ApplicationController
+ def an_action
+ render_class_and_action
+ end
+ def action_with_layout
+ render_class_and_action(nil, :layout => "plugin_layout")
+ end
+end
--- /dev/null
+class AppAndPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from alpha_plugin'
+ end
+end
--- /dev/null
+class Namespace::AlphaPluginController < ApplicationController
+ def an_action
+ render_class_and_action
+ end
+end
\ No newline at end of file
--- /dev/null
+class Namespace::AppAndPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from alpha_plugin'
+ end
+end
--- /dev/null
+class Namespace::SharedPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from alpha_plugin'
+ end
+end
--- /dev/null
+class SharedEngineController < ApplicationController
+ def an_action
+ render_class_and_action 'from alpha_engine'
+ end
+end
--- /dev/null
+class AlphaPluginModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+class AppAndPluginModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+
+ def defined_only_in_alpha_plugin_version
+ # should not be defined as the model in app/models takes precedence
+ end
+end
\ No newline at end of file
--- /dev/null
+class SharedPluginModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %>
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from a_view)
\ No newline at end of file
--- /dev/null
+<%= yield %> (with plugin layout)
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %>
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %>
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from alpha_plugin)
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from alpha_plugin)
\ No newline at end of file
--- /dev/null
+class AlphaPluginLibModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+class AppAndPluginLibModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+
+ def defined_only_in_alpha_plugin_version
+ # should not be defined
+ end
+end
\ No newline at end of file
--- /dev/null
+en:
+ hello: "Hello from alfa"
+ plugin: "alfa"
--- /dev/null
+class AppAndPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from beta_plugin'
+ end
+end
--- /dev/null
+class Namespace::SharedPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from beta_plugin'
+ end
+end
--- /dev/null
+class SharedPluginController < ApplicationController
+ def an_action
+ render_class_and_action 'from beta_plugin'
+ end
+end
--- /dev/null
+class SharedPluginModel < ActiveRecord::Base
+ def self.report_location; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from beta_plugin)
\ No newline at end of file
--- /dev/null
+<%= TestHelper.view_path_for __FILE__ %> (from beta_plugin)
\ No newline at end of file
--- /dev/null
+# just here so that Rails recognizes this as a plugin
\ No newline at end of file
--- /dev/null
+en:
+ hello: "Hello from beta"
+ plugin: "beta"
--- /dev/null
+class AssetsController < ApplicationController
+end
\ No newline at end of file
--- /dev/null
+<%= image_tag 'image.png', :plugin => 'test_assets' %>
+<%= javascript_include_tag 'file.1.js', 'file2', :plugin => "test_assets" %>
+<%= stylesheet_link_tag 'file.1.css', 'file2', :plugin => "test_assets" %>
+<%= image_submit_tag 'image.png', :plugin => "test_assets" %>
--- /dev/null
+<div id="assets_layout">
+ <%= yield %>
+</div>
\ No newline at end of file
--- /dev/null
+class Thing
+ def self.from_plugin; TestHelper::report_location(__FILE__); end
+end
\ No newline at end of file
--- /dev/null
+# just here so that Rails recognizes this as a plugin
\ No newline at end of file
--- /dev/null
+class CreateTests < ActiveRecord::Migration
+ def self.up
+ create_table 'tests' do |t|
+ t.column 'name', :string
+ end
+ end
+
+ def self.down
+ drop_table 'tests'
+ end
+end
--- /dev/null
+class CreateOthers < ActiveRecord::Migration
+ def self.up
+ create_table 'others' do |t|
+ t.column 'name', :string
+ end
+ end
+
+ def self.down
+ drop_table 'others'
+ end
+end
--- /dev/null
+class CreateExtras < ActiveRecord::Migration
+ def self.up
+ create_table 'extras' do |t|
+ t.column 'name', :string
+ end
+ end
+
+ def self.down
+ drop_table 'extras'
+ end
+end
--- /dev/null
+class PluginMail < ActionMailer::Base
+ def mail_from_plugin(note=nil)
+ body(:note => note)
+ end
+
+ def mail_from_plugin_with_application_template(note=nil)
+ body(:note => note)
+ end
+
+ def multipart_from_plugin
+ content_type 'multipart/alternative'
+ part :content_type => "text/html", :body => render_message("multipart_from_plugin_html", {})
+ part "text/plain" do |p|
+ p.body = render_message("multipart_from_plugin_plain", {})
+ end
+ end
+
+ def multipart_from_plugin_with_application_template
+ content_type 'multipart/alternative'
+ part :content_type => "text/html", :body => render_message("multipart_from_plugin_with_application_template_html", {})
+ part "text/plain" do |p|
+ p.body = render_message("multipart_from_plugin_with_application_template_plain", {})
+ end
+ end
+
+end
\ No newline at end of file
--- /dev/null
+<%= @note %>
\ No newline at end of file
--- /dev/null
+html template
\ No newline at end of file
--- /dev/null
+plain template
\ No newline at end of file
--- /dev/null
+template from plugin
\ No newline at end of file
--- /dev/null
+template from plugin
\ No newline at end of file
--- /dev/null
+class Namespace::TestRoutingController < ApplicationController
+ def routed_action
+ render_class_and_action
+ end
+end
\ No newline at end of file
--- /dev/null
+class TestRoutingController < ApplicationController
+ def routed_action
+ render_class_and_action
+ end
+
+ def test_named_routes_from_plugin
+ render :text => plugin_route_path(:action => "index")
+ end
+end
\ No newline at end of file
--- /dev/null
+ActionController::Routing::Routes.draw do |map|
+ map.connect 'routes/:action', :controller => "test_routing"
+ map.plugin_route 'somespace/routes/:action', :controller => "namespace/test_routing"
+end
\ No newline at end of file
--- /dev/null
+Fixtures are only copied from plugins with an +app+ directory, but git needs this directory to be non-empty
\ No newline at end of file
--- /dev/null
+require File.expand_path(File.join(File.dirname(__FILE__), *%w[.. .. .. .. .. test test_helper]))
+
+class OverrideTest < ActiveSupport::TestCase
+ def test_overrides_from_the_application_should_work
+ flunk "this test should be overridden by the app"
+ end
+
+ def test_tests_within_the_plugin_should_still_run
+ assert true, "non-overridden plugin tests should still run"
+ end
+end
+
+Engines::Testing.override_tests_from_app
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ActionMailerWithinApplicationTest < Test::Unit::TestCase
+
+ def test_normal_implicit_template
+ m = NotifyMail.create_signup("hello")
+ assert m.body =~ /^Signup template from application/
+ end
+
+ def test_action_mailer_can_get_helper
+ m = NotifyMail.create_signup('James')
+ assert m.body =~ /James/
+ assert m.body =~ /semaJ/ # from the helper
+ end
+
+ def test_multipart_mails_with_explicit_templates
+ m = NotifyMail.create_multipart
+ assert_equal 2, m.parts.length
+ assert_equal 'the html part of the email james', m.parts[0].body
+ assert_equal 'the plaintext part of the email', m.parts[1].body
+ end
+
+ def test_multipart_mails_with_implicit_templates
+ m = NotifyMail.create_implicit_multipart
+ assert_equal 2, m.parts.length
+ assert_equal 'the implicit plaintext part of the email', m.parts[0].body
+ assert_equal 'the implicit html part of the email james', m.parts[1].body
+ end
+end
+
+
+class ActionMailerWithinPluginsTest < Test::Unit::TestCase
+ def test_should_be_able_to_create_mails_from_plugin
+ m = PluginMail.create_mail_from_plugin("from_plugin")
+ assert_equal "from_plugin", m.body
+ end
+
+ def test_should_be_able_to_overload_views_within_the_application
+ m = PluginMail.create_mail_from_plugin_with_application_template("from_plugin")
+ assert_equal "from_plugin (from application)", m.body
+ end
+
+ def test_should_be_able_to_create_a_multipart_mail_from_within_plugin
+ m = PluginMail.create_multipart_from_plugin
+ assert_equal 2, m.parts.length
+ assert_equal 'html template', m.parts[0].body
+ assert_equal 'plain template', m.parts[1].body
+ end
+
+ def test_plugin_mailer_template_overriding
+ m = PluginMail.create_multipart_from_plugin_with_application_template
+ assert_equal 'plugin mail template loaded from application', m.parts[1].body
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ArbitraryCodeMixingTest < Test::Unit::TestCase
+ def setup
+ Engines.code_mixing_file_types = %w(controller helper)
+ end
+
+ def test_should_allow_setting_of_different_code_mixing_file_types
+ assert_nothing_raised {
+ Engines.mix_code_from :things
+ }
+ end
+
+ def test_should_add_new_types_to_existing_code_mixing_file_types
+ Engines.mix_code_from :things
+ assert_equal ["controller", "helper", "thing"], Engines.code_mixing_file_types
+ Engines.mix_code_from :other
+ assert_equal ["controller", "helper", "thing", "other"], Engines.code_mixing_file_types
+ end
+
+ def test_should_allow_setting_of_multiple_types_at_once
+ Engines.mix_code_from :things, :other
+ assert_equal ["controller", "helper", "thing", "other"], Engines.code_mixing_file_types
+ end
+
+ def test_should_singularize_elements_to_be_mixed
+ # this is the only test using mocha, so let's try to work around it
+ # also, this seems to be already tested with the :things in the tests above
+ # arg = stub(:to_s => stub(:singularize => "element"))
+ Engines.mix_code_from :elements
+ assert Engines.code_mixing_file_types.include?("element")
+ end
+
+ # TODO doesn't seem to work as expected?
+
+ # def test_should_successfully_mix_custom_types
+ # Engines.mix_code_from :things
+ # assert_equal 'Thing (from app)', Thing.from_app
+ # assert_equal 'Thing (from test_code_mixing)', Thing.from_plugin
+ # end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AssetsTest < Test::Unit::TestCase
+ def setup
+ Engines::Assets.mirror_files_for Engines.plugins[:test_assets]
+ end
+
+ def teardown
+ FileUtils.rm_r(Engines.public_directory) if File.exist?(Engines.public_directory)
+ end
+
+ def test_engines_has_created_base_public_file
+ assert File.exist?(Engines.public_directory)
+ end
+
+ def test_engines_has_created_README_in_public_directory
+ assert File.exist?(File.join(Engines.public_directory, 'README'))
+ end
+
+ def test_public_files_have_been_copied_from_test_assets_plugin
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets', 'file.txt'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets', 'subfolder'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets', 'subfolder', 'file_in_subfolder.txt'))
+ end
+
+ def test_engines_has_not_created_duplicated_file_structure
+ assert !File.exists?(File.join(Engines.public_directory, "test_assets", RAILS_ROOT))
+ end
+
+ def test_public_files_have_been_copied_from_test_assets_with_assets_dir_plugin
+ Engines::Assets.mirror_files_for Engines.plugins[:test_assets_with_assets_directory]
+
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_assets_directory'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_assets_directory', 'file.txt'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_assets_directory', 'subfolder'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_assets_directory', 'subfolder', 'file_in_subfolder.txt'))
+ end
+
+ def test_public_files_have_been_copied_from_test_assets_with_no_subdirectory_plugin
+ Engines::Assets.mirror_files_for Engines.plugins[:test_assets_with_no_subdirectory]
+
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_no_subdirectory'))
+ assert File.exist?(File.join(Engines.public_directory, 'test_assets_with_no_subdirectory', 'file.txt'))
+ end
+
+ def test_public_files_have_NOT_been_copied_from_plugins_without_public_or_asset_directories
+ Engines::Assets.mirror_files_for Engines.plugins[:alpha_plugin]
+
+ assert !File.exist?(File.join(Engines.public_directory, 'alpha_plugin'))
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class BackwardsCompatibilityTest < Test::Unit::TestCase
+ def test_rails_module_plugin_method_should_delegate_to_engines_plugins
+ assert_nothing_raised { Rails.plugins }
+ assert_equal Engines.plugins, Rails.plugins
+ end
+end
\ No newline at end of file
--- /dev/null
+# Tests in this file ensure that:
+#
+# * the application /app/[controllers|helpers|models] and /lib
+# paths preceed the corresponding plugin paths
+# * the plugin paths are added to $LOAD_PATH in the order in which plugins are
+# loaded
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class LoadPathTest < Test::Unit::TestCase
+ def setup
+ @load_path = expand_paths($LOAD_PATH)
+ end
+
+ # Not sure if these test actually make sense as this now essentially tests
+ # Rails core functionality. On the other hand Engines relies on this to some
+ # extend so this will choke if something important changes in Rails.
+
+ # the application app/... and lib/ directories should appear
+ # before any plugin directories
+
+ def test_application_app_libs_should_precede_all_plugin_app_libs
+ types = %w(app/controllers app/helpers app/models lib)
+ types.each do |t|
+ app_index = load_path_index(File.join(RAILS_ROOT, t))
+ assert_not_nil app_index, "#{t} is missing in $LOAD_PATH"
+ Engines.plugins.each do |plugin|
+ first_plugin_index = load_path_index(File.join(plugin.directory, t))
+ assert(app_index < first_plugin_index) unless first_plugin_index.nil?
+ end
+ end
+ end
+
+ # the engine directories should appear in the proper order based on
+ # the order they were started
+
+ def test_plugin_dirs_should_appear_in_reverse_plugin_loading_order
+ app_paths = %w(app/controllers/ app app/models app/helpers lib)
+ app_paths.map { |p| File.join(RAILS_ROOT, p)}
+ plugin_paths = Engines.plugins.reverse.collect { |plugin| plugin.load_paths.reverse }.flatten
+
+ expected_paths = expand_paths(app_paths + plugin_paths)
+ # only look at those paths that are also present in expected_paths so
+ # the only difference would be in the order of the paths
+ actual_paths = @load_path & expected_paths
+
+ assert_equal expected_paths, actual_paths
+ end
+
+ protected
+ def expand_paths(paths)
+ paths.collect { |p| File.expand_path(p) }
+ end
+
+ def load_path_index(dir)
+ @load_path.index(File.expand_path(dir))
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+require 'rails_generator'
+require 'rails_generator/scripts/generate'
+
+class MigrationsTest < Test::Unit::TestCase
+
+ @@migration_dir = "#{RAILS_ROOT}/db/migrate"
+
+ def setup
+ ActiveRecord::Migration.verbose = false
+ Engines.plugins[:test_migration].migrate(0)
+ end
+
+ def teardown
+ FileUtils.rm_r(@@migration_dir) if File.exist?(@@migration_dir)
+ end
+
+ def test_engine_migrations_can_run_down
+ assert !table_exists?('tests'), ActiveRecord::Base.connection.tables.inspect
+ assert !table_exists?('others'), ActiveRecord::Base.connection.tables.inspect
+ assert !table_exists?('extras'), ActiveRecord::Base.connection.tables.inspect
+ end
+
+ def test_engine_migrations_can_run_up
+ Engines.plugins[:test_migration].migrate(3)
+ assert table_exists?('tests')
+ assert table_exists?('others')
+ assert table_exists?('extras')
+ end
+
+ def test_engine_migrations_can_upgrade_incrementally
+ Engines.plugins[:test_migration].migrate(1)
+ assert table_exists?('tests')
+ assert !table_exists?('others')
+ assert !table_exists?('extras')
+ assert_equal 1, Engines::Plugin::Migrator.current_version(Engines.plugins[:test_migration])
+
+
+ Engines.plugins[:test_migration].migrate(2)
+ assert table_exists?('others')
+ assert_equal 2, Engines::Plugin::Migrator.current_version(Engines.plugins[:test_migration])
+
+
+ Engines.plugins[:test_migration].migrate(3)
+ assert table_exists?('extras')
+ assert_equal 3, Engines::Plugin::Migrator.current_version(Engines.plugins[:test_migration])
+ end
+
+ def test_generator_creates_plugin_migration_file
+ Rails::Generator::Scripts::Generate.new.run(['plugin_migration', 'test_migration'], :quiet => true)
+ assert migration_file, "migration file is missing"
+ end
+
+ private
+
+ def table_exists?(table)
+ ActiveRecord::Base.connection.tables.include?(table)
+ end
+
+ def migration_file
+ Dir["#{@@migration_dir}/*test_migration_to_version_3.rb"][0]
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ModelAndLibTest < Test::Unit::TestCase
+
+ def test_WITH_a_model_defined_only_in_a_plugin_IT_should_load_the_model
+ assert_equal 'AlphaPluginModel (from alpha_plugin)', AlphaPluginModel.report_location
+ end
+
+ def test_WITH_a_model_defined_only_in_a_plugin_lib_dir_IT_should_load_the_model
+ assert_equal 'AlphaPluginLibModel (from alpha_plugin)', AlphaPluginLibModel.report_location
+ end
+
+ # app takes precedence over plugins
+
+ def test_WITH_a_model_defined_in_both_app_and_plugin_IT_should_load_the_one_in_app
+ assert_equal 'AppAndPluginModel (from app)', AppAndPluginModel.report_location
+ assert_raises(NoMethodError) { AppAndPluginLibModel.defined_only_in_alpha_engine_version }
+ end
+
+ def test_WITH_a_model_defined_in_both_app_and_plugin_lib_dirs_IT_should_load_the_one_in_app
+ assert_equal 'AppAndPluginLibModel (from lib)', AppAndPluginLibModel.report_location
+ assert_raises(NoMethodError) { AppAndPluginLibModel.defined_only_in_alpha_engine_version }
+ end
+
+ # subsequently loaded plugins take precendence over previously loaded plugins
+
+ # TODO
+ #
+ # this does work when we rely on $LOAD_PATH while it won't work when we use
+ # Dependency constant autoloading. This somewhat confusing difference has
+ # been there since at least Rails 1.2.x. See http://www.ruby-forum.com/topic/134529
+
+ def test_WITH_a_model_defined_in_two_plugins_IT_should_load_the_latter_of_both
+ require 'shared_plugin_model'
+ assert_equal SharedPluginModel.report_location, 'SharedPluginModel (from beta_plugin)'
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PluginsTest < Test::Unit::TestCase
+
+ def test_should_allow_access_to_plugins_by_strings_or_symbols
+ p = Engines.plugins["alpha_plugin"]
+ q = Engines.plugins[:alpha_plugin]
+ assert_kind_of Engines::Plugin, p
+ assert_equal p, q
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.join(File.dirname(__FILE__), *%w[.. .. test_helper])
+
+class OverrideTest < ActiveSupport::TestCase
+ def test_overrides_from_the_application_should_work
+ assert true, "overriding plugin tests from the application should work"
+ end
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TestingTest < Test::Unit::TestCase
+ def setup
+ Engines::Testing.set_fixture_path
+ @filename = File.join(Engines::Testing.temporary_fixtures_directory, 'testing_fixtures.yml')
+ File.delete(@filename) if File.exists?(@filename)
+ end
+
+ def teardown
+ File.delete(@filename) if File.exists?(@filename)
+ end
+
+ def test_should_copy_fixtures_files_to_tmp_directory
+ assert !File.exists?(@filename)
+ Engines::Testing.setup_plugin_fixtures
+ assert File.exists?(@filename)
+ end
+end
\ No newline at end of file
--- /dev/null
+Copyright (c) 2007 West Arete Computing, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
--- /dev/null
+== Gravatar Plugin
+
+This plugin provides a handful of view helpers for displaying gravatars
+(globally-recognized avatars).
+
+Gravatars allow users to configure an avatar to go with their email address at
+a central location: http://gravatar.com. Gravatar-aware websites (such
+as yours) can then look up and display each user's preferred avatar, without
+having to handle avatar management. The user gets the benefit of not having to
+set up an avatar for each site that they post on.
+
+== Installation
+
+ cd ~/myapp
+ ruby script/plugin install git://github.com/woods/gravatar-plugin.git
+
+or, if you're using piston[http://piston.rubyforge.org] (worth it!):
+
+ cd ~/myapp/vendor/plugins
+ piston import git://github.com/woods/gravatar-plugin.git
+
+== Example
+
+If you represent your users with a model that has an +email+ method (typical
+for most rails authentication setups), then you can simply use this method
+in your views:
+
+ <%= gravatar_for @user %>
+
+This will be replaced with the full HTML +img+ tag necessary for displaying
+that user's gravatar.
+
+Other helpers are documented under GravatarHelper::PublicMethods.
+
+== Acknowledgments
+
+Thanks to Magnus Bergmark (http://github.com/Mange), who contributed the SSL
+support in this plugin, as well as a few minor fixes.
+
+The following people have also written gravatar-related Ruby libraries:
+* Seth Rasmussen created the gravatar gem[http://gravatar.rubyforge.org]
+* Matt McCray has also created a gravatar
+ plugin[http://mattmccray.com/svn/rails/plugins/gravatar_helper]
+
+== Author
+
+ Scott A. Woods
+ West Arete Computing, Inc.
+ http://westarete.com
+ scott at westarete dot com
+
+== TODO
+
+* Add specs for ssl support
+* Finish rdoc documentation
\ No newline at end of file
--- /dev/null
+require 'spec/rake/spectask'
+require 'rake/rdoctask'
+
+desc 'Default: run all specs'
+task :default => :spec
+
+desc 'Run all application-specific specs'
+Spec::Rake::SpecTask.new(:spec) do |t|
+ t.rcov = true
+end
+
+desc "Report code statistics (KLOCs, etc) from the application"
+task :stats do
+ RAILS_ROOT = File.dirname(__FILE__)
+ STATS_DIRECTORIES = [
+ %w(Libraries lib/),
+ %w(Specs spec/),
+ ].collect { |name, dir| [ name, "#{RAILS_ROOT}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
+ require 'code_statistics'
+ CodeStatistics.new(*STATS_DIRECTORIES).to_s
+end
+
+namespace :doc do
+ desc 'Generate documentation for the assert_request plugin.'
+ Rake::RDocTask.new(:plugin) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Gravatar Rails Plugin'
+ rdoc.options << '--line-numbers' << '--inline-source' << '--accessor' << 'cattr_accessor=rw'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ end
+end
--- /dev/null
+author: Scott Woods, West Arete Computing
+summary: View helpers for displaying gravatars.
+homepage: http://github.com/woods/gravatar-plugin/
+plugin: git://github.com/woods/gravatar-plugin.git
+license: MIT
+version: 0.1
+rails_version: 1.0+
--- /dev/null
+require 'gravatar'
+ActionView::Base.send :include, GravatarHelper::PublicMethods
--- /dev/null
+require 'digest/md5'
+require 'cgi'
+
+module GravatarHelper
+
+ # These are the options that control the default behavior of the public
+ # methods. They can be overridden during the actual call to the helper,
+ # or you can set them in your environment.rb as such:
+ #
+ # # Allow racier gravatars
+ # GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
+ #
+ DEFAULT_OPTIONS = {
+ # The URL of a default image to display if the given email address does
+ # not have a gravatar.
+ :default => nil,
+
+ # The default size in pixels for the gravatar image (they're square).
+ :size => 50,
+
+ # The maximum allowed MPAA rating for gravatars. This allows you to
+ # exclude gravatars that may be out of character for your site.
+ :rating => 'PG',
+
+ # The alt text to use in the img tag for the gravatar. Since it's a
+ # decorational picture, the alt text should be empty according to the
+ # XHTML specs.
+ :alt => '',
+
+ # The class to assign to the img tag for the gravatar.
+ :class => 'gravatar',
+
+ # Whether or not to display the gravatars using HTTPS instead of HTTP
+ :ssl => false,
+ }
+
+ # The methods that will be made available to your views.
+ module PublicMethods
+
+ # Return the HTML img tag for the given user's gravatar. Presumes that
+ # the given user object will respond_to "email", and return the user's
+ # email address.
+ def gravatar_for(user, options={})
+ gravatar(user.email, options)
+ end
+
+ # Return the HTML img tag for the given email address's gravatar.
+ def gravatar(email, options={})
+ src = h(gravatar_url(email, options))
+ options = DEFAULT_OPTIONS.merge(options)
+ [:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) }
+ "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
+ end
+
+ # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
+ # a secure URL will be used instead. This is required when the gravatar is to be
+ # displayed on a HTTPS site.
+ def gravatar_api_url(hash, ssl=false)
+ if ssl
+ "https://secure.gravatar.com/avatar/#{hash}"
+ else
+ "http://www.gravatar.com/avatar/#{hash}"
+ end
+ end
+
+ # Return the gravatar URL for the given email address.
+ def gravatar_url(email, options={})
+ email_hash = Digest::MD5.hexdigest(email)
+ options = DEFAULT_OPTIONS.merge(options)
+ options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
+ returning gravatar_api_url(email_hash, options.delete(:ssl)) do |url|
+ opts = []
+ [:rating, :size, :default].each do |opt|
+ unless options[opt].nil?
+ value = h(options[opt])
+ opts << [opt, value].join('=')
+ end
+ end
+ url << "?#{opts.join('&')}" unless opts.empty?
+ end
+ end
+
+ end
+
+end
\ No newline at end of file
--- /dev/null
+require 'rubygems'
+require 'erb' # to get "h"
+require 'active_support' # to get "returning"
+require File.dirname(__FILE__) + '/../lib/gravatar'
+include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
+
+context "gravatar_url with a custom default URL" do
+ setup do
+ @original_options = DEFAULT_OPTIONS.dup
+ DEFAULT_OPTIONS[:default] = "no_avatar.png"
+ @url = gravatar_url("somewhere")
+ end
+
+ specify "should include the \"default\" argument in the result" do
+ @url.should match(/&default=no_avatar.png/)
+ end
+
+ teardown do
+ DEFAULT_OPTIONS.merge!(@original_options)
+ end
+
+end
+
+context "gravatar_url with default settings" do
+ setup do
+ @url = gravatar_url("somewhere")
+ end
+
+ specify "should have a nil default URL" do
+ DEFAULT_OPTIONS[:default].should be_nil
+ end
+
+ specify "should not include the \"default\" argument in the result" do
+ @url.should_not match(/&default=/)
+ end
+
+end
\ No newline at end of file
--- /dev/null
+* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
+
+* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
+
+* Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]
+
+* Add Timeout protection [Rick]
+
+* An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.
+
+* Allow a return_to option to be used instead of the requested url [Josh Peek]
+
+* Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]
+
+* Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]
+
+* Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]
+
+* Allow -'s in #normalize_url [Rick]
+
+* remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]
+
+* Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]
+
+* Just use the path for the return URL, so extra query parameters don't interfere [DHH]
+
+* Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]
+
+* Added normalize_url and applied it to all operations going through the plugin [DHH]
+
+* Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]
+
+* Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]
+
+* Stop relying on root_url being defined, we can just grab the current url instead [DHH]
\ No newline at end of file
--- /dev/null
+OpenIdAuthentication
+====================
+
+Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
+
+ gem install ruby-openid
+
+To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
+from that gem.
+
+The specification used is http://openid.net/specs/openid-authentication-2_0.html.
+
+
+Prerequisites
+=============
+
+OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of
+database tables to store the authentication keys. So you'll have to run the migration to create these before you get started:
+
+ rake open_id_authentication:db:create
+
+Or, use the included generators to install or upgrade:
+
+ ./script/generate open_id_authentication_tables MigrationName
+ ./script/generate upgrade_open_id_authentication_tables MigrationName
+
+Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
+
+ OpenIdAuthentication.store = :file
+
+This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
+If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
+
+The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
+
+ map.root :controller => 'articles'
+
+This plugin relies on Rails Edge revision 6317 or newer.
+
+
+Example
+=======
+
+This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
+salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
+not a destination.
+
+Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
+model you are using for authentication.
+
+Also of note is the following code block used in the example below:
+
+ authenticate_with_open_id do |result, identity_url|
+ ...
+ end
+
+In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
+If you are storing just 'example.com' with your user, the lookup will fail.
+
+There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
+
+ OpenIdAuthentication.normalize_url(user.identity_url)
+
+The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
+It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
+Use the above code in your User model and validate OpenID URLs before saving them.
+
+config/routes.rb
+
+ map.root :controller => 'articles'
+ map.resource :session
+
+
+app/views/sessions/new.erb
+
+ <% form_tag(session_url) do %>
+ <p>
+ <label for="name">Username:</label>
+ <%= text_field_tag "name" %>
+ </p>
+
+ <p>
+ <label for="password">Password:</label>
+ <%= password_field_tag %>
+ </p>
+
+ <p>
+ ...or use:
+ </p>
+
+ <p>
+ <label for="openid_identifier">OpenID:</label>
+ <%= text_field_tag "openid_identifier" %>
+ </p>
+
+ <p>
+ <%= submit_tag 'Sign in', :disable_with => "Signing in…" %>
+ </p>
+ <% end %>
+
+app/controllers/sessions_controller.rb
+ class SessionsController < ApplicationController
+ def create
+ if using_open_id?
+ open_id_authentication
+ else
+ password_authentication(params[:name], params[:password])
+ end
+ end
+
+
+ protected
+ def password_authentication(name, password)
+ if @current_user = @account.users.authenticate(params[:name], params[:password])
+ successful_login
+ else
+ failed_login "Sorry, that username/password doesn't work"
+ end
+ end
+
+ def open_id_authentication
+ authenticate_with_open_id do |result, identity_url|
+ if result.successful?
+ if @current_user = @account.users.find_by_identity_url(identity_url)
+ successful_login
+ else
+ failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
+ end
+ else
+ failed_login result.message
+ end
+ end
+ end
+
+
+ private
+ def successful_login
+ session[:user_id] = @current_user.id
+ redirect_to(root_url)
+ end
+
+ def failed_login(message)
+ flash[:error] = message
+ redirect_to(new_session_url)
+ end
+ end
+
+
+
+If you're fine with the result messages above and don't need individual logic on a per-failure basis,
+you can collapse the case into a mere boolean:
+
+ def open_id_authentication
+ authenticate_with_open_id do |result, identity_url|
+ if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
+ successful_login
+ else
+ failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
+ end
+ end
+ end
+
+
+Simple Registration OpenID Extension
+====================================
+
+Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
+
+You can support it in your app by changing #open_id_authentication
+
+ def open_id_authentication(identity_url)
+ # Pass optional :required and :optional keys to specify what sreg fields you want.
+ # Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
+ authenticate_with_open_id(identity_url,
+ :required => [ :nickname, :email ],
+ :optional => :fullname) do |result, identity_url, registration|
+ case result.status
+ when :missing
+ failed_login "Sorry, the OpenID server couldn't be found"
+ when :invalid
+ failed_login "Sorry, but this does not appear to be a valid OpenID"
+ when :canceled
+ failed_login "OpenID verification was canceled"
+ when :failed
+ failed_login "Sorry, the OpenID verification failed"
+ when :successful
+ if @current_user = @account.users.find_by_identity_url(identity_url)
+ assign_registration_attributes!(registration)
+
+ if current_user.save
+ successful_login
+ else
+ failed_login "Your OpenID profile registration failed: " +
+ @current_user.errors.full_messages.to_sentence
+ end
+ else
+ failed_login "Sorry, no user by that identity URL exists"
+ end
+ end
+ end
+ end
+
+ # registration is a hash containing the valid sreg keys given above
+ # use this to map them to fields of your user model
+ def assign_registration_attributes!(registration)
+ model_to_registration_mapping.each do |model_attribute, registration_attribute|
+ unless registration[registration_attribute].blank?
+ @current_user.send("#{model_attribute}=", registration[registration_attribute])
+ end
+ end
+ end
+
+ def model_to_registration_mapping
+ { :login => 'nickname', :email => 'email', :display_name => 'fullname' }
+ end
+
+Attribute Exchange OpenID Extension
+===================================
+
+Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
+
+Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
+
+ authenticate_with_open_id(identity_url,
+ :required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
+
+This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
+
+
+
+Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
\ No newline at end of file
--- /dev/null
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the open_id_authentication plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the open_id_authentication plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'OpenIdAuthentication'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
--- /dev/null
+class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
+ def initialize(runtime_args, runtime_options = {})
+ super
+ end
+
+ def manifest
+ record do |m|
+ m.migration_template 'migration.rb', 'db/migrate'
+ end
+ end
+end
--- /dev/null
+class <%= class_name %> < ActiveRecord::Migration
+ def self.up
+ create_table :open_id_authentication_associations, :force => true do |t|
+ t.integer :issued, :lifetime
+ t.string :handle, :assoc_type
+ t.binary :server_url, :secret
+ end
+
+ create_table :open_id_authentication_nonces, :force => true do |t|
+ t.integer :timestamp, :null => false
+ t.string :server_url, :null => true
+ t.string :salt, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :open_id_authentication_associations
+ drop_table :open_id_authentication_nonces
+ end
+end
--- /dev/null
+class <%= class_name %> < ActiveRecord::Migration
+ def self.up
+ drop_table :open_id_authentication_settings
+ drop_table :open_id_authentication_nonces
+
+ create_table :open_id_authentication_nonces, :force => true do |t|
+ t.integer :timestamp, :null => false
+ t.string :server_url, :null => true
+ t.string :salt, :null => false
+ end
+ end
+
+ def self.down
+ drop_table :open_id_authentication_nonces
+
+ create_table :open_id_authentication_nonces, :force => true do |t|
+ t.integer :created
+ t.string :nonce
+ end
+
+ create_table :open_id_authentication_settings, :force => true do |t|
+ t.string :setting
+ t.binary :value
+ end
+ end
+end
--- /dev/null
+class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
+ def initialize(runtime_args, runtime_options = {})
+ super
+ end
+
+ def manifest
+ record do |m|
+ m.migration_template 'migration.rb', 'db/migrate'
+ end
+ end
+end
--- /dev/null
+begin\r
+ require 'openid'\r
+rescue LoadError\r
+ begin\r
+ gem 'ruby-openid', '>=2.1.4'\r
+ rescue Gem::LoadError\r
+ # no openid support\r
+ end\r
+end\r
+\r
+if Object.const_defined?(:OpenID)\r
+ config.to_prepare do\r
+ OpenID::Util.logger = Rails.logger\r
+ ActionController::Base.send :include, OpenIdAuthentication\r
+ end\r
+end\r
--- /dev/null
+require 'uri'
+require 'openid/extensions/sreg'
+require 'openid/extensions/ax'
+require 'openid/store/filesystem'
+
+require File.dirname(__FILE__) + '/open_id_authentication/db_store'
+require File.dirname(__FILE__) + '/open_id_authentication/mem_cache_store'
+require File.dirname(__FILE__) + '/open_id_authentication/request'
+require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
+
+module OpenIdAuthentication
+ OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
+
+ def self.store
+ @@store
+ end
+
+ def self.store=(*store_option)
+ store, *parameters = *([ store_option ].flatten)
+
+ @@store = case store
+ when :db
+ OpenIdAuthentication::DbStore.new
+ when :mem_cache
+ OpenIdAuthentication::MemCacheStore.new(*parameters)
+ when :file
+ OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
+ else
+ raise "Unknown store: #{store}"
+ end
+ end
+
+ self.store = :db
+
+ class InvalidOpenId < StandardError
+ end
+
+ class Result
+ ERROR_MESSAGES = {
+ :missing => "Sorry, the OpenID server couldn't be found",
+ :invalid => "Sorry, but this does not appear to be a valid OpenID",
+ :canceled => "OpenID verification was canceled",
+ :failed => "OpenID verification failed",
+ :setup_needed => "OpenID verification needs setup"
+ }
+
+ def self.[](code)
+ new(code)
+ end
+
+ def initialize(code)
+ @code = code
+ end
+
+ def status
+ @code
+ end
+
+ ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
+
+ def successful?
+ @code == :successful
+ end
+
+ def unsuccessful?
+ ERROR_MESSAGES.keys.include?(@code)
+ end
+
+ def message
+ ERROR_MESSAGES[@code]
+ end
+ end
+
+ # normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
+ def self.normalize_identifier(identifier)
+ # clean up whitespace
+ identifier = identifier.to_s.strip
+
+ # if an XRI has a prefix, strip it.
+ identifier.gsub!(/xri:\/\//i, '')
+
+ # dodge XRIs -- TODO: validate, don't just skip.
+ unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
+ # does it begin with http? if not, add it.
+ identifier = "http://#{identifier}" unless identifier =~ /^http/i
+
+ # strip any fragments
+ identifier.gsub!(/\#(.*)$/, '')
+
+ begin
+ uri = URI.parse(identifier)
+ uri.scheme = uri.scheme.downcase # URI should do this
+ identifier = uri.normalize.to_s
+ rescue URI::InvalidURIError
+ raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
+ end
+ end
+
+ return identifier
+ end
+
+ # deprecated for OpenID 2.0, where not all OpenIDs are URLs
+ def self.normalize_url(url)
+ ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
+ self.normalize_identifier(url)
+ end
+
+ protected
+ def normalize_url(url)
+ OpenIdAuthentication.normalize_url(url)
+ end
+
+ def normalize_identifier(url)
+ OpenIdAuthentication.normalize_identifier(url)
+ end
+
+ # The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
+ # because that's what the specification dictates in order to get browser auto-complete working across sites
+ def using_open_id?(identity_url = nil) #:doc:
+ identity_url ||= params[:openid_identifier] || params[:openid_url]
+ !identity_url.blank? || params[:open_id_complete]
+ end
+
+ def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
+ identity_url ||= params[:openid_identifier] || params[:openid_url]
+
+ if params[:open_id_complete].nil?
+ begin_open_id_authentication(identity_url, options, &block)
+ else
+ complete_open_id_authentication(&block)
+ end
+ end
+
+ private
+ def begin_open_id_authentication(identity_url, options = {})
+ identity_url = normalize_identifier(identity_url)
+ return_to = options.delete(:return_to)
+ method = options.delete(:method)
+
+ options[:required] ||= [] # reduces validation later
+ options[:optional] ||= []
+
+ open_id_request = open_id_consumer.begin(identity_url)
+ add_simple_registration_fields(open_id_request, options)
+ add_ax_fields(open_id_request, options)
+ redirect_to(open_id_redirect_url(open_id_request, return_to, method))
+ rescue OpenIdAuthentication::InvalidOpenId => e
+ yield Result[:invalid], identity_url, nil
+ rescue OpenID::OpenIDError, Timeout::Error => e
+ logger.error("[OPENID] #{e}")
+ yield Result[:missing], identity_url, nil
+ end
+
+ def complete_open_id_authentication
+ params_with_path = params.reject { |key, value| request.path_parameters[key] }
+ params_with_path.delete(:format)
+ open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
+ identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
+
+ case open_id_response.status
+ when OpenID::Consumer::SUCCESS
+ profile_data = {}
+
+ # merge the SReg data and the AX data into a single hash of profile data
+ [ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
+ if data_response.from_success_response( open_id_response )
+ profile_data.merge! data_response.from_success_response( open_id_response ).data
+ end
+ end
+
+ yield Result[:successful], identity_url, profile_data
+ when OpenID::Consumer::CANCEL
+ yield Result[:canceled], identity_url, nil
+ when OpenID::Consumer::FAILURE
+ yield Result[:failed], identity_url, nil
+ when OpenID::Consumer::SETUP_NEEDED
+ yield Result[:setup_needed], open_id_response.setup_url, nil
+ end
+ end
+
+ def open_id_consumer
+ OpenID::Consumer.new(session, OpenIdAuthentication.store)
+ end
+
+ def add_simple_registration_fields(open_id_request, fields)
+ sreg_request = OpenID::SReg::Request.new
+
+ # filter out AX identifiers (URIs)
+ required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
+ optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
+
+ sreg_request.request_fields(required_fields, true) unless required_fields.blank?
+ sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
+ sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
+ open_id_request.add_extension(sreg_request)
+ end
+
+ def add_ax_fields( open_id_request, fields )
+ ax_request = OpenID::AX::FetchRequest.new
+
+ # look through the :required and :optional fields for URIs (AX identifiers)
+ fields[:required].each do |f|
+ next unless f =~ /^https?:\/\//
+ ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
+ end
+
+ fields[:optional].each do |f|
+ next unless f =~ /^https?:\/\//
+ ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
+ end
+
+ open_id_request.add_extension( ax_request )
+ end
+
+ def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
+ open_id_request.return_to_args['_method'] = (method || request.method).to_s
+ open_id_request.return_to_args['open_id_complete'] = '1'
+ open_id_request.redirect_url(root_url, return_to || requested_url)
+ end
+
+ def requested_url
+ relative_url_root = self.class.respond_to?(:relative_url_root) ?
+ self.class.relative_url_root.to_s :
+ request.relative_url_root
+ "#{request.protocol}#{request.host_with_port}#{relative_url_root}#{request.path}"
+ end
+
+ def timeout_protection_from_identity_server
+ yield
+ rescue Timeout::Error
+ Class.new do
+ def status
+ OpenID::FAILURE
+ end
+
+ def msg
+ "Identity server timed out"
+ end
+ end.new
+ end
+end
--- /dev/null
+module OpenIdAuthentication
+ class Association < ActiveRecord::Base
+ set_table_name :open_id_authentication_associations
+
+ def from_record
+ OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
+ end
+ end
+end
--- /dev/null
+require 'openid/store/interface'
+
+module OpenIdAuthentication
+ class DbStore < OpenID::Store::Interface
+ def self.cleanup_nonces
+ now = Time.now.to_i
+ Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
+ end
+
+ def self.cleanup_associations
+ now = Time.now.to_i
+ Association.delete_all(['issued + lifetime > ?',now])
+ end
+
+ def store_association(server_url, assoc)
+ remove_association(server_url, assoc.handle)
+ Association.create(:server_url => server_url,
+ :handle => assoc.handle,
+ :secret => assoc.secret,
+ :issued => assoc.issued,
+ :lifetime => assoc.lifetime,
+ :assoc_type => assoc.assoc_type)
+ end
+
+ def get_association(server_url, handle = nil)
+ assocs = if handle.blank?
+ Association.find_all_by_server_url(server_url)
+ else
+ Association.find_all_by_server_url_and_handle(server_url, handle)
+ end
+
+ assocs.reverse.each do |assoc|
+ a = assoc.from_record
+ if a.expires_in == 0
+ assoc.destroy
+ else
+ return a
+ end
+ end if assocs.any?
+
+ return nil
+ end
+
+ def remove_association(server_url, handle)
+ Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
+ end
+
+ def use_nonce(server_url, timestamp, salt)
+ return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
+ return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
+ Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt)
+ return true
+ end
+ end
+end
--- /dev/null
+require 'digest/sha1'
+require 'openid/store/interface'
+
+module OpenIdAuthentication
+ class MemCacheStore < OpenID::Store::Interface
+ def initialize(*addresses)
+ @connection = ActiveSupport::Cache::MemCacheStore.new(addresses)
+ end
+
+ def store_association(server_url, assoc)
+ server_key = association_server_key(server_url)
+ assoc_key = association_key(server_url, assoc.handle)
+
+ assocs = @connection.read(server_key) || {}
+ assocs[assoc.issued] = assoc_key
+
+ @connection.write(server_key, assocs)
+ @connection.write(assoc_key, assoc, :expires_in => assoc.lifetime)
+ end
+
+ def get_association(server_url, handle = nil)
+ if handle
+ @connection.read(association_key(server_url, handle))
+ else
+ server_key = association_server_key(server_url)
+ assocs = @connection.read(server_key)
+ return if assocs.nil?
+
+ last_key = assocs[assocs.keys.sort.last]
+ @connection.read(last_key)
+ end
+ end
+
+ def remove_association(server_url, handle)
+ server_key = association_server_key(server_url)
+ assoc_key = association_key(server_url, handle)
+ assocs = @connection.read(server_key)
+
+ return false unless assocs && assocs.has_value?(assoc_key)
+
+ assocs = assocs.delete_if { |key, value| value == assoc_key }
+
+ @connection.write(server_key, assocs)
+ @connection.delete(assoc_key)
+
+ return true
+ end
+
+ def use_nonce(server_url, timestamp, salt)
+ return false if @connection.read(nonce_key(server_url, salt))
+ return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
+ @connection.write(nonce_key(server_url, salt), timestamp, :expires_in => OpenID::Nonce.skew)
+ return true
+ end
+
+ private
+ def association_key(server_url, handle = nil)
+ "openid_association_#{digest(server_url)}_#{digest(handle)}"
+ end
+
+ def association_server_key(server_url)
+ "openid_association_server_#{digest(server_url)}"
+ end
+
+ def nonce_key(server_url, salt)
+ "openid_nonce_#{digest(server_url)}_#{digest(salt)}"
+ end
+
+ def digest(text)
+ Digest::SHA1.hexdigest(text)
+ end
+ end
+end
--- /dev/null
+module OpenIdAuthentication
+ class Nonce < ActiveRecord::Base
+ set_table_name :open_id_authentication_nonces
+ end
+end
--- /dev/null
+module OpenIdAuthentication
+ module Request
+ def self.included(base)
+ base.alias_method_chain :request_method, :openid
+ end
+
+ def request_method_with_openid
+ if !parameters[:_method].blank? && parameters[:open_id_complete] == '1'
+ parameters[:_method].to_sym
+ else
+ request_method_without_openid
+ end
+ end
+ end
+end
+
+# In Rails 2.3, the request object has been renamed
+# from AbstractRequest to Request
+if defined? ActionController::Request
+ ActionController::Request.send :include, OpenIdAuthentication::Request
+else
+ ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request
+end
--- /dev/null
+# http://trac.openidenabled.com/trac/ticket/156
+module OpenID
+ @@timeout_threshold = 20
+
+ def self.timeout_threshold
+ @@timeout_threshold
+ end
+
+ def self.timeout_threshold=(value)
+ @@timeout_threshold = value
+ end
+
+ class StandardFetcher
+ def make_http(uri)
+ http = @proxy.new(uri.host, uri.port)
+ http.read_timeout = http.open_timeout = OpenID.timeout_threshold
+ http
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+namespace :open_id_authentication do
+ namespace :db do
+ desc "Creates authentication tables for use with OpenIdAuthentication"
+ task :create => :environment do
+ generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"])
+ end
+
+ desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x"
+ task :upgrade => :environment do
+ generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"])
+ end
+
+ def generate_migration(args)
+ require 'rails_generator'
+ require 'rails_generator/scripts/generate'
+
+ if ActiveRecord::Base.connection.supports_migrations?
+ Rails::Generator::Scripts::Generate.new.run(args)
+ else
+ raise "Task unavailable to this database (no migration support)"
+ end
+ end
+
+ desc "Clear the authentication tables"
+ task :clear => :environment do
+ OpenIdAuthentication::DbStore.cleanup_nonces
+ OpenIdAuthentication::DbStore.cleanup_associations
+ end
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/test_helper'
+require File.dirname(__FILE__) + '/../lib/open_id_authentication/mem_cache_store'
+
+# Mock MemCacheStore with MemoryStore for testing
+class OpenIdAuthentication::MemCacheStore < OpenID::Store::Interface
+ def initialize(*addresses)
+ @connection = ActiveSupport::Cache::MemoryStore.new
+ end
+end
+
+class MemCacheStoreTest < Test::Unit::TestCase
+ ALLOWED_HANDLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
+
+ def setup
+ @store = OpenIdAuthentication::MemCacheStore.new
+ end
+
+ def test_store
+ server_url = "http://www.myopenid.com/openid"
+ assoc = gen_assoc(0)
+
+ # Make sure that a missing association returns no result
+ assert_retrieve(server_url)
+
+ # Check that after storage, getting returns the same result
+ @store.store_association(server_url, assoc)
+ assert_retrieve(server_url, nil, assoc)
+
+ # more than once
+ assert_retrieve(server_url, nil, assoc)
+
+ # Storing more than once has no ill effect
+ @store.store_association(server_url, assoc)
+ assert_retrieve(server_url, nil, assoc)
+
+ # Removing an association that does not exist returns not present
+ assert_remove(server_url, assoc.handle + 'x', false)
+
+ # Removing an association that does not exist returns not present
+ assert_remove(server_url + 'x', assoc.handle, false)
+
+ # Removing an association that is present returns present
+ assert_remove(server_url, assoc.handle, true)
+
+ # but not present on subsequent calls
+ assert_remove(server_url, assoc.handle, false)
+
+ # Put assoc back in the store
+ @store.store_association(server_url, assoc)
+
+ # More recent and expires after assoc
+ assoc2 = gen_assoc(1)
+ @store.store_association(server_url, assoc2)
+
+ # After storing an association with a different handle, but the
+ # same server_url, the handle with the later expiration is returned.
+ assert_retrieve(server_url, nil, assoc2)
+
+ # We can still retrieve the older association
+ assert_retrieve(server_url, assoc.handle, assoc)
+
+ # Plus we can retrieve the association with the later expiration
+ # explicitly
+ assert_retrieve(server_url, assoc2.handle, assoc2)
+
+ # More recent, and expires earlier than assoc2 or assoc. Make sure
+ # that we're picking the one with the latest issued date and not
+ # taking into account the expiration.
+ assoc3 = gen_assoc(2, 100)
+ @store.store_association(server_url, assoc3)
+
+ assert_retrieve(server_url, nil, assoc3)
+ assert_retrieve(server_url, assoc.handle, assoc)
+ assert_retrieve(server_url, assoc2.handle, assoc2)
+ assert_retrieve(server_url, assoc3.handle, assoc3)
+
+ assert_remove(server_url, assoc2.handle, true)
+
+ assert_retrieve(server_url, nil, assoc3)
+ assert_retrieve(server_url, assoc.handle, assoc)
+ assert_retrieve(server_url, assoc2.handle, nil)
+ assert_retrieve(server_url, assoc3.handle, assoc3)
+
+ assert_remove(server_url, assoc2.handle, false)
+ assert_remove(server_url, assoc3.handle, true)
+
+ assert_retrieve(server_url, nil, assoc)
+ assert_retrieve(server_url, assoc.handle, assoc)
+ assert_retrieve(server_url, assoc2.handle, nil)
+ assert_retrieve(server_url, assoc3.handle, nil)
+
+ assert_remove(server_url, assoc2.handle, false)
+ assert_remove(server_url, assoc.handle, true)
+ assert_remove(server_url, assoc3.handle, false)
+
+ assert_retrieve(server_url, nil, nil)
+ assert_retrieve(server_url, assoc.handle, nil)
+ assert_retrieve(server_url, assoc2.handle, nil)
+ assert_retrieve(server_url, assoc3.handle, nil)
+
+ assert_remove(server_url, assoc2.handle, false)
+ assert_remove(server_url, assoc.handle, false)
+ assert_remove(server_url, assoc3.handle, false)
+ end
+
+ def test_nonce
+ server_url = "http://www.myopenid.com/openid"
+
+ [server_url, ''].each do |url|
+ nonce1 = OpenID::Nonce::mk_nonce
+
+ assert_nonce(nonce1, true, url, "#{url}: nonce allowed by default")
+ assert_nonce(nonce1, false, url, "#{url}: nonce not allowed twice")
+ assert_nonce(nonce1, false, url, "#{url}: nonce not allowed third time")
+
+ # old nonces shouldn't pass
+ old_nonce = OpenID::Nonce::mk_nonce(3600)
+ assert_nonce(old_nonce, false, url, "Old nonce #{old_nonce.inspect} passed")
+ end
+ end
+
+ private
+ def gen_assoc(issued, lifetime = 600)
+ secret = OpenID::CryptUtil.random_string(20, nil)
+ handle = OpenID::CryptUtil.random_string(128, ALLOWED_HANDLE)
+ OpenID::Association.new(handle, secret, Time.now + issued, lifetime, 'HMAC-SHA1')
+ end
+
+ def assert_retrieve(url, handle = nil, expected = nil)
+ assoc = @store.get_association(url, handle)
+
+ if expected.nil?
+ assert_nil(assoc)
+ else
+ assert_equal(expected, assoc)
+ assert_equal(expected.handle, assoc.handle)
+ assert_equal(expected.secret, assoc.secret)
+ end
+ end
+
+ def assert_remove(url, handle, expected)
+ present = @store.remove_association(url, handle)
+ assert_equal(expected, present)
+ end
+
+ def assert_nonce(nonce, expected, server_url, msg = "")
+ stamp, salt = OpenID::Nonce::split_nonce(nonce)
+ actual = @store.use_nonce(server_url, stamp, salt)
+ assert_equal(expected, actual, msg)
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/test_helper'
+
+class NormalizeTest < Test::Unit::TestCase
+ include OpenIdAuthentication
+
+ NORMALIZATIONS = {
+ "openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
+ "http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
+ "https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler",
+ "HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER",
+ "HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER",
+ "loudthinking.com" => "http://loudthinking.com/",
+ "http://loudthinking.com" => "http://loudthinking.com/",
+ "http://loudthinking.com:80" => "http://loudthinking.com/",
+ "https://loudthinking.com:443" => "https://loudthinking.com/",
+ "http://loudthinking.com:8080" => "http://loudthinking.com:8080/",
+ "techno-weenie.net" => "http://techno-weenie.net/",
+ "http://techno-weenie.net" => "http://techno-weenie.net/",
+ "http://techno-weenie.net " => "http://techno-weenie.net/",
+ "=name" => "=name"
+ }
+
+ def test_normalizations
+ NORMALIZATIONS.each do |from, to|
+ assert_equal to, normalize_identifier(from)
+ end
+ end
+
+ def test_broken_open_id
+ assert_raises(InvalidOpenId) { normalize_identifier(nil) }
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/test_helper'
+
+class OpenIdAuthenticationTest < Test::Unit::TestCase
+ def setup
+ @controller = Class.new do
+ include OpenIdAuthentication
+ def params() {} end
+ end.new
+ end
+
+ def test_authentication_should_fail_when_the_identity_server_is_missing
+ open_id_consumer = mock()
+ open_id_consumer.expects(:begin).raises(OpenID::OpenIDError)
+ @controller.expects(:open_id_consumer).returns(open_id_consumer)
+ @controller.expects(:logger).returns(mock(:error => true))
+
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
+ assert result.missing?
+ assert_equal "Sorry, the OpenID server couldn't be found", result.message
+ end
+ end
+
+ def test_authentication_should_be_invalid_when_the_identity_url_is_invalid
+ @controller.send(:authenticate_with_open_id, "!") do |result, identity_url|
+ assert result.invalid?, "Result expected to be invalid but was not"
+ assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message
+ end
+ end
+
+ def test_authentication_should_fail_when_the_identity_server_times_out
+ open_id_consumer = mock()
+ open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.")
+ @controller.expects(:open_id_consumer).returns(open_id_consumer)
+ @controller.expects(:logger).returns(mock(:error => true))
+
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
+ assert result.missing?
+ assert_equal "Sorry, the OpenID server couldn't be found", result.message
+ end
+ end
+
+ def test_authentication_should_begin_when_the_identity_server_is_present
+ @controller.expects(:begin_open_id_authentication)
+ @controller.send(:authenticate_with_open_id, "http://someone.example.com")
+ end
+end
--- /dev/null
+require File.dirname(__FILE__) + '/test_helper'
+
+class StatusTest < Test::Unit::TestCase
+ include OpenIdAuthentication
+
+ def test_state_conditional
+ assert Result[:missing].missing?
+ assert Result[:missing].unsuccessful?
+ assert !Result[:missing].successful?
+
+ assert Result[:successful].successful?
+ assert !Result[:successful].unsuccessful?
+ end
+end
\ No newline at end of file
--- /dev/null
+require 'test/unit'
+require 'rubygems'
+
+gem 'activesupport'
+require 'active_support'
+
+gem 'actionpack'
+require 'action_controller'
+
+gem 'mocha'
+require 'mocha'
+
+gem 'ruby-openid'
+require 'openid'
+
+RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT
+require File.dirname(__FILE__) + "/../lib/open_id_authentication"
--- /dev/null
+module PrependEngineViews
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+ base.class_eval do
+ alias_method_chain :add_engine_view_paths, :prepend
+ end
+ end
+
+ module InstanceMethods
+ # Patch Rails so engine's views are prepended to the view_path,
+ # thereby letting plugins override application views
+ def add_engine_view_paths_with_prepend
+ paths = ActionView::PathSet.new(engines.collect(&:view_path))
+ ActionController::Base.view_paths.unshift(*paths)
+ ActionMailer::Base.view_paths.unshift(*paths) if configuration.frameworks.include?(:action_mailer)
+ end
+ end
+end
+
+Rails::Plugin::Loader.send :include, PrependEngineViews
+
--- /dev/null
+1.00 Added view template functionality
+1.10 Added Chinese support
+1.11 Added Japanese support
+1.12 Added Korean support
+1.13 Updated to fpdf.rb 1.53d.
+ Added makefont and fpdf_eps.
+ Handle \n at the beginning of a string in MultiCell.
+ Tried to fix clipping issue in MultiCell - still needs some work.
+1.14 2006-09-26
+* Added support for @options_for_rfpdf hash for configuration:
+ * Added :filename option in this hash
+If you're using the same settings for @options_for_rfpdf often, you might want to
+put your assignment in a before_filter (perhaps overriding :filename, etc in your actions).
--- /dev/null
+Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND
+NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
--- /dev/null
+= RFPDF Template Plugin
+
+A template plugin allowing the inclusion of ERB-enabled RFPDF template files.
+
+== Example .rb method Usage
+
+In the controller, something like:
+
+ def mypdf
+ pdf = FPDF.new()
+
+ #
+ # Chinese
+ #
+ pdf.extend(PDF_Chinese)
+ pdf.AddPage
+ pdf.AddBig5Font
+ pdf.SetFont('Big5','',18)
+ pdf.Write(5, '²{®É®ð·Å 18 C Àã«× 83 %')
+ icBig5 = Iconv.new('Big5', 'UTF-8')
+ pdf.Write(15, icBig5.iconv("宋体 should be working"))
+ send_data pdf.Output, :filename => "something.pdf", :type => "application/pdf"
+ end
+
+== Example .rfdf Usage
+
+In the controller, something like:
+
+ def mypdf
+ @options_for_rfpdf ||= {}
+ @options_for_rfpdf[:file_name] = "nice_looking.pdf"
+ end
+
+In the layout (make sure this is the only item in the layout):
+<%= @content_for_layout %>
+
+In the view (mypdf.rfpdf):
+
+<%
+ pdf = FPDF.new()
+ #
+ # Chinese
+ #
+ pdf.extend(PDF_Chinese)
+ pdf.AddPage
+ pdf.AddBig5Font
+ pdf.SetFont('Big5','',18)
+ pdf.Write(5, '²{®É®ð·Å 18 C Àã«× 83 %')
+ icBig5 = Iconv.new('Big5', 'UTF-8')
+ pdf.Write(15, icBig5.iconv("宋体 should be working"))
+
+ #
+ # Japanese
+ #
+ pdf.extend(PDF_Japanese)
+ pdf.AddSJISFont();
+ pdf.AddPage();
+ pdf.SetFont('SJIS','',18);
+ pdf.Write(5,'9ÉñåéÇÃåˆäJÉeÉXÉgÇåoǃPHP 3.0ÇÕ1998îN6åéÇ…åˆéÆÇ…ÉäÉäÅ[ÉXÇ≥ÇÍNjǵÇΩÅB');
+ icSJIS = Iconv.new('SJIS', 'UTF-8')
+ pdf.Write(15, icSJIS.iconv("これはテキストである should be working"))
+
+ #
+ # Korean
+ #
+ pdf.extend(PDF_Korean)
+ pdf.AddUHCFont();
+ pdf.AddPage();
+ pdf.SetFont('UHC','',18);
+ pdf.Write(5,'PHP 3.0Àº 1998³â 6¿ù¿¡ °ø½ÄÀûÀ¸·Î ¸±¸®ÁîµÇ¾ú´Ù. °ø°³ÀûÀÎ Å×½ºÆ® ÀÌÈľà 9°³¿ù¸¸À̾ú´Ù.');
+ icUHC = Iconv.new('UHC', 'UTF-8')
+ pdf.Write(15, icUHC.iconv("이것은 원본 이다"))
+
+ #
+ # English
+ #
+ pdf.AddPage();
+ pdf.SetFont('Arial', '', 10)
+ pdf.Write(5, "should be working")
+%>
+<%= pdf.Output() %>
+
+
+== Configuring
+
+You can configure Rfpdf by using an @options_for_rfpdf hash in your controllers.
+
+Here are a few options:
+
+:filename (default: action_name.pdf)
+ Filename of PDF to generate
+
+Note: If you're using the same settings for @options_for_rfpdf often, you might want to
+put your assignment in a before_filter (perhaps overriding :filename, etc in your actions).
+
+== Problems
+
+Layouts and partials are currently not supported; just need
+to wrap the PDF generation differently.
--- /dev/null
+require 'rfpdf'
+
+begin
+ ActionView::Template::register_template_handler 'rfpdf', RFPDF::View
+rescue NameError
+ # Rails < 2.1
+ RFPDF::View.backward_compatibility_mode = true
+ ActionView::Base::register_template_handler 'rfpdf', RFPDF::View
+end
--- /dev/null
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+#
+# The MIT License
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+
+require 'rfpdf/errors'
+require 'rfpdf/view'
+require 'rfpdf/fpdf'
+require 'rfpdf/rfpdf'
+require 'rfpdf/chinese'
+require 'rfpdf/japanese'
+require 'rfpdf/korean'
--- /dev/null
+# Translation of the bookmark class from the PHP FPDF script from Olivier Plathey
+# Translated by Sylvain Lafleur and ?? with the help of Brian Ollenberger
+#
+# First added in 1.53b
+#
+# Usage is as follows:
+#
+# require 'fpdf'
+# require 'bookmark'
+# pdf = FPDF.new
+# pdf.extend(PDF_Bookmark)
+#
+# This allows it to be combined with other extensions, such as the Chinese
+# module.
+
+module PDF_Bookmark
+ def PDF_Bookmark.extend_object(o)
+ o.instance_eval('@outlines,@OutlineRoot=[],0')
+ super(o)
+ end
+
+ def Bookmark(txt,level=0,y=0)
+ y=self.GetY() if y==-1
+ @outlines.push({'t'=>txt,'l'=>level,'y'=>y,'p'=>self.PageNo()})
+ end
+
+ def putbookmarks
+ @nb=@outlines.size
+ return if @nb==0
+ lru=[]
+ level=0
+ @outlines.each_index do |i|
+ o=@outlines[i]
+ if o['l']>0
+ parent=lru[o['l']-1]
+ # Set parent and last pointers
+ @outlines[i]['parent']=parent
+ @outlines[parent]['last']=i
+ if o['l']>level
+ # Level increasing: set first pointer
+ @outlines[parent]['first']=i
+ end
+ else
+ @outlines[i]['parent']=@nb
+ end
+ if o['l']<=level and i>0
+ # Set prev and next pointers
+ prev=lru[o['l']]
+ @outlines[prev]['next']=i
+ @outlines[i]['prev']=prev
+ end
+ lru[o['l']]=i
+ level=o['l']
+ end
+ # Outline items
+ n=@n+1
+ @outlines.each_index do |i|
+ o=@outlines[i]
+ newobj
+ out('<</Title '+(textstring(o['t'])))
+ out('/Parent '+(n+o['parent']).to_s+' 0 R')
+ if o['prev']
+ out('/Prev '+(n+o['prev']).to_s+' 0 R')
+ end
+ if o['next']
+ out('/Next '+(n+o['next']).to_s+' 0 R')
+ end
+ if o['first']
+ out('/First '+(n+o['first']).to_s+' 0 R')
+ end
+ if o['last']
+ out('/Last '+(n+o['last']).to_s+' 0 R')
+ end
+ out(sprintf('/Dest [%d 0 R /XYZ 0 %.2f
+null]',1+2*o['p'],(@h-o['y'])*@k))
+ out('/Count 0>>')
+ out('endobj')
+ end
+ # Outline root
+ newobj
+ @OutlineRoot=@n
+ out('<</Type /Outlines /First '+n.to_s+' 0 R')
+ out('/Last '+(n+lru[0]).to_s+' 0 R>>')
+ out('endobj')
+ end
+
+ def putresources
+ super
+ putbookmarks
+ end
+
+ def putcatalog
+ super
+ if not @outlines.empty?
+ out('/Outlines '+@OutlineRoot.to_s+' 0 R')
+ out('/PageMode /UseOutlines')
+ end
+ end
+end
--- /dev/null
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>\r
+# 1.12 contributed by Ed Moss.\r
+#\r
+# The MIT License\r
+#\r
+# Permission is hereby granted, free of charge, to any person obtaining a copy\r
+# of this software and associated documentation files (the "Software"), to deal\r
+# in the Software without restriction, including without limitation the rights\r
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+# copies of the Software, and to permit persons to whom the Software is\r
+# furnished to do so, subject to the following conditions:\r
+#\r
+# The above copyright notice and this permission notice shall be included in\r
+# all copies or substantial portions of the Software.\r
+#\r
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+# THE SOFTWARE.\r
+#\r
+# This is direct port of chinese.php\r
+#\r
+# Chinese PDF support.\r
+#\r
+# Usage is as follows:\r
+#\r
+# require 'fpdf'\r
+# require 'chinese'\r
+# pdf = FPDF.new\r
+# pdf.extend(PDF_Chinese)\r
+#\r
+# This allows it to be combined with other extensions, such as the bookmark\r
+# module.\r
+\r
+module PDF_Chinese\r
+\r
+ Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250,\r
+ '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500,\r
+ '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250,\r
+ '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625,\r
+ 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823,\r
+ 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677,\r
+ 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427,\r
+ 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802,\r
+ 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677,\r
+ 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667}\r
+\r
+ GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239,\r
+ '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462,\r
+ '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238,\r
+ '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563,\r
+ 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772,\r
+ 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620,\r
+ 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427,\r
+ 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793,\r
+ 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652,\r
+ 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605}\r
+\r
+ def AddCIDFont(family,style,name,cw,cMap,registry)\r
+#ActionController::Base::logger.debug registry.to_a.join(":").to_s\r
+ fontkey=family.downcase+style.upcase\r
+ unless @fonts[fontkey].nil?\r
+ Error("Font already added: family style")\r
+ end\r
+ i=@fonts.length+1\r
+ name=name.gsub(' ','')\r
+ @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry}\r
+ end\r
+\r
+ def AddCIDFonts(family,name,cw,cMap,registry)\r
+ AddCIDFont(family,'',name,cw,cMap,registry)\r
+ AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)\r
+ AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)\r
+ AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)\r
+ end\r
+\r
+ def AddBig5Font(family='Big5',name='MSungStd-Light-Acro')\r
+ #Add Big5 font with proportional Latin\r
+ cw=Big5_widths\r
+ cMap='ETenms-B5-H'\r
+ registry={'ordering'=>'CNS1','supplement'=>0}\r
+#ActionController::Base::logger.debug registry.to_a.join(":").to_s\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro')\r
+ #Add Big5 font with half-witdh Latin\r
+ cw = {}\r
+ 32.upto(126) do |i|\r
+ cw[i.chr]=500\r
+ end\r
+ cMap='ETen-B5-H'\r
+ registry={'ordering'=>'CNS1','supplement'=>0}\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def AddGBFont(family='GB',name='STSongStd-Light-Acro')\r
+ #Add GB font with proportional Latin\r
+ cw=GB_widths\r
+ cMap='GBKp-EUC-H'\r
+ registry={'ordering'=>'GB1','supplement'=>2}\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro')\r
+ #Add GB font with half-width Latin\r
+ 32.upto(126) do |i|\r
+ cw[i.chr]=500\r
+ end\r
+ cMap='GBK-EUC-H'\r
+ registry={'ordering'=>'GB1','supplement'=>2}\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def GetStringWidth(s)\r
+ if(@CurrentFont['type']=='Type0')\r
+ return GetMBStringWidth(s)\r
+ else\r
+ return super(s)\r
+ end\r
+ end\r
+\r
+ def GetMBStringWidth(s)\r
+ #Multi-byte version of GetStringWidth()\r
+ l=0\r
+ cw=@CurrentFont['cw']\r
+ nb=s.length\r
+ i=0\r
+ while(i<nb)\r
+ c=s[i]\r
+ if(c<128)\r
+ l+=cw[c.chr] if cw[c.chr]\r
+ i+=1\r
+ else\r
+ l+=1000\r
+ i+=2\r
+ end\r
+ end\r
+ return l*@FontSize/1000\r
+ end\r
+\r
+ def MultiCell(w,h,txt,border=0,align='L',fill=0)\r
+ if(@CurrentFont['type']=='Type0')\r
+ MBMultiCell(w,h,txt,border,align,fill)\r
+ else\r
+ super(w,h,txt,border,align,fill)\r
+ end\r
+ end\r
+\r
+ def MBMultiCell(w,h,txt,border=0,align='L',fill=0)\r
+ #Multi-byte version of MultiCell()\r
+ cw=@CurrentFont['cw']\r
+ if(w==0)\r
+ w=@w-@rMargin-@x\r
+ end\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ s=txt.gsub("\r",'')\r
+ nb=s.length\r
+ if(nb>0 and s[nb-1]=="\n")\r
+ nb-=1\r
+ end\r
+ b=0\r
+ if(border)\r
+ if(border==1)\r
+ border='LTRB'\r
+ b='LRT'\r
+ b2='LR'\r
+ else\r
+ b2=''\r
+ if(border.to_s.index('L'))\r
+ b2+='L'\r
+ end\r
+ if(border.to_s.index('R'))\r
+ b2+='R'\r
+ end\r
+ b=border.to_s.index('T') ? b2+'T' : b2\r
+ end\r
+ end\r
+ sep=-1\r
+ i=0\r
+ j=0\r
+ l=0\r
+ nl=1\r
+ while(i<nb)\r
+ #Get next character\r
+ c=s[i]\r
+ #Check if ASCII or MB\r
+ ascii=(c<128)\r
+ if(c.chr=="\n")\r
+ #Explicit line break\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ i+=1\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ nl+=1\r
+ if(border and nl==2)\r
+ b=b2\r
+ end\r
+ next\r
+ end\r
+ if(!ascii)\r
+ sep=i\r
+ ls=l\r
+ elsif(c==' ')\r
+ sep=i\r
+ ls=l\r
+ end\r
+ l+=ascii ? (cw[c.chr] || 0) : 1100\r
+ if(l>wmax)\r
+ #Automatic line break\r
+ if(sep==-1 or i==j)\r
+ if(i==j)\r
+ i+=ascii ? 1 : 3\r
+ end\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ else\r
+ Cell(w,h,s[j,sep-j],b,2,align,fill)\r
+ i=(s[sep]==' ') ? sep+1 : sep\r
+ end\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+# nl+=1\r
+ if(border and nl==2)\r
+ b=b2\r
+ end\r
+ else\r
+ i+=ascii ? 1 : 3\r
+ end\r
+ end\r
+ #Last chunk\r
+ if(border and not border.to_s.index('B').nil?)\r
+ b+='B'\r
+ end\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ @x=@lMargin\r
+ end\r
+\r
+ def Write(h,txt,link='')\r
+ if(@CurrentFont['type']=='Type0')\r
+ MBWrite(h,txt,link)\r
+ else\r
+ super(h,txt,link)\r
+ end\r
+ end\r
+\r
+ def MBWrite(h,txt,link)\r
+ #Multi-byte version of Write()\r
+ cw=@CurrentFont['cw']\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ s=txt.gsub("\r",'')\r
+ nb=s.length\r
+ sep=-1\r
+ i=0\r
+ j=0\r
+ l=0\r
+ nl=1\r
+ while(i<nb)\r
+ #Get next character\r
+ c=s[i]\r
+ #Check if ASCII or MB\r
+ ascii=(c<128)\r
+ if(c.chr=="\n")\r
+ #Explicit line break\r
+ Cell(w,h,s[j,i-j],0,2,'',0,link)\r
+ i+=1\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ if(nl==1)\r
+ @x=@lMargin\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ end\r
+ nl+=1\r
+ next\r
+ end\r
+ if(!ascii or c==' ')\r
+ sep=i\r
+ end\r
+ l+=ascii ? cw[c.chr] : 1100\r
+ if(l>wmax)\r
+ #Automatic line break\r
+ if(sep==-1 or i==j)\r
+ if(@x>@lMargin)\r
+ #Move to next line\r
+ @x=@lMargin\r
+ @y+=h\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ i+=1\r
+ nl+=1\r
+ next\r
+ end\r
+ if(i==j)\r
+ i+=ascii ? 1 : 3\r
+ end\r
+ Cell(w,h,s[j,i-j],0,2,'',0,link)\r
+ else\r
+ Cell(w,h,s[j,sep-j],0,2,'',0,link)\r
+ i=(s[sep]==' ') ? sep+1 : sep\r
+ end\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ if(nl==1)\r
+ @x=@lMargin\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ end\r
+ nl+=1\r
+ else\r
+ i+=ascii ? 1 : 3\r
+ end\r
+ end\r
+ #Last chunk\r
+ if(i!=j)\r
+ Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)\r
+ end\r
+ end\r
+\r
+private\r
+\r
+ def putfonts()\r
+ nf=@n\r
+ @diffs.each do |diff|\r
+ #Encodings\r
+ newobj()\r
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')\r
+ out('endobj')\r
+ end\r
+ # mqr=get_magic_quotes_runtime()\r
+ # set_magic_quotes_runtime(0)\r
+ @FontFiles.each_pair do |file, info|\r
+ #Font file embedding\r
+ newobj()\r
+ @FontFiles[file]['n']=@n\r
+ if(defined('FPDF_FONTPATH'))\r
+ file=FPDF_FONTPATH+file\r
+ end\r
+ size=filesize(file)\r
+ if(!size)\r
+ Error('Font file not found')\r
+ end\r
+ out('<</Length '+size)\r
+ if(file[-2]=='.z')\r
+ out('/Filter /FlateDecode')\r
+ end\r
+ out('/Length1 '+info['length1'])\r
+ unless info['length2'].nil?\r
+ out('/Length2 '+info['length2']+' /Length3 0')\r
+ end\r
+ out('>>')\r
+ f=fopen(file,'rb')\r
+ putstream(fread(f,size))\r
+ fclose(f)\r
+ out('endobj')\r
+ end\r
+#\r
+ # set_magic_quotes_runtime(mqr)\r
+#\r
+ @fonts.each_pair do |k, font|\r
+ #Font objects\r
+ newobj()\r
+ @fonts[k]['n']=@n\r
+ out('<</Type /Font')\r
+ if(font['type']=='Type0')\r
+ putType0(font)\r
+ else\r
+ name=font['name']\r
+ out('/BaseFont /'+name)\r
+ if(font['type']=='core')\r
+ #Standard font\r
+ out('/Subtype /Type1')\r
+ if(name!='Symbol' and name!='ZapfDingbats')\r
+ out('/Encoding /WinAnsiEncoding')\r
+ end\r
+ else\r
+ #Additional font\r
+ out('/Subtype /'+font['type'])\r
+ out('/FirstChar 32')\r
+ out('/LastChar 255')\r
+ out('/Widths '+(@n+1)+' 0 R')\r
+ out('/FontDescriptor '+(@n+2)+' 0 R')\r
+ if(font['enc'])\r
+ if !font['diff'].nil?\r
+ out('/Encoding '+(nf+font['diff'])+' 0 R')\r
+ else\r
+ out('/Encoding /WinAnsiEncoding')\r
+ end\r
+ end\r
+ end\r
+ out('>>')\r
+ out('endobj')\r
+ if(font['type']!='core')\r
+ #Widths\r
+ newobj()\r
+ cw=font['cw']\r
+ s='['\r
+ 32.upto(255) do |i|\r
+ s+=cw[i.chr]+' '\r
+ end\r
+ out(s+']')\r
+ out('endobj')\r
+ #Descriptor\r
+ newobj()\r
+ s='<</Type /FontDescriptor /FontName /'+name\r
+ font['desc'].each_pair do |k, v|\r
+ s+=' /'+k+' '+v\r
+ end\r
+ file=font['file']\r
+ if(file)\r
+ s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'\r
+ end\r
+ out(s+'>>')\r
+ out('endobj')\r
+ end\r
+ end\r
+ end\r
+ end\r
+\r
+ def putType0(font)\r
+ #Type0\r
+ out('/Subtype /Type0')\r
+ out('/BaseFont /'+font['name']+'-'+font['CMap'])\r
+ out('/Encoding /'+font['CMap'])\r
+ out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')\r
+ out('>>')\r
+ out('endobj')\r
+ #CIDFont\r
+ newobj()\r
+ out('<</Type /Font')\r
+ out('/Subtype /CIDFontType0')\r
+ out('/BaseFont /'+font['name'])\r
+ out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')\r
+ out('/FontDescriptor '+(@n+1).to_s+' 0 R')\r
+ if(font['CMap']=='ETen-B5-H')\r
+ w='13648 13742 500'\r
+ elsif(font['CMap']=='GBK-EUC-H')\r
+ w='814 907 500 7716 [500]'\r
+ else\r
+ # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s\r
+ # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s\r
+ w='1 ['\r
+ font['cw'].keys.sort.each {|key|\r
+ w+=font['cw'][key].to_s + " "\r
+# ActionController::Base::logger.debug key.to_s\r
+# ActionController::Base::logger.debug font['cw'][key].to_s\r
+ }\r
+ w +=']'\r
+ end\r
+ out('/W ['+w+']>>')\r
+ out('endobj')\r
+ #Font descriptor\r
+ newobj()\r
+ out('<</Type /FontDescriptor')\r
+ out('/FontName /'+font['name'])\r
+ out('/Flags 6')\r
+ out('/FontBBox [0 -200 1000 900]')\r
+ out('/ItalicAngle 0')\r
+ out('/Ascent 800')\r
+ out('/Descent -200')\r
+ out('/CapHeight 800')\r
+ out('/StemV 50')\r
+ out('>>')\r
+ out('endobj')\r
+ end\r
+end\r
--- /dev/null
+module RFPDF
+ class GenerationError < StandardError #:nodoc:
+ end
+end
\ No newline at end of file
--- /dev/null
+# Ruby FPDF 1.53d
+# FPDF 1.53 by Olivier Plathey ported to Ruby by Brian Ollenberger
+# Copyright 2005 Brian Ollenberger
+# Please retain this entire copyright notice. If you distribute any
+# modifications, place an additional comment here that clearly indicates
+# that it was modified. You may (but are not send any useful modifications that you make
+# back to me at http://zeropluszero.com/software/fpdf/
+
+# Bug fixes, examples, external fonts, JPEG support, and upgrade to version
+# 1.53 contributed by Kim Shrier.
+#
+# Bookmark support contributed by Sylvain Lafleur.
+#
+# EPS support contributed by Thiago Jackiw, ported from the PHP version by Valentin Schmidt.
+#
+# Bookmarks contributed by Sylvain Lafleur.
+#
+# 1.53 contributed by Ed Moss
+# Handle '\n' at the beginning of a string
+# Bookmarks contributed by Sylvain Lafleur.
+
+require 'date'
+require 'zlib'
+
+class FPDF
+ FPDF_VERSION = '1.53d'
+
+ Charwidths = {
+ 'courier'=>[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600],
+
+ 'courierB'=>[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600],
+
+ 'courierI'=>[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600],
+
+ 'courierBI'=>[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600],
+
+ 'helvetica'=>[278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500],
+
+ 'helveticaB'=>[278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556],
+
+ 'helveticaI'=>[278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500],
+
+ 'helveticaBI'=>[278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556],
+
+ 'times'=>[250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 350, 500, 350, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 350, 611, 350, 350, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 350, 444, 722, 250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500],
+
+ 'timesB'=>[250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 350, 667, 350, 350, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 444, 722, 250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500],
+
+ 'timesI'=>[250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 350, 500, 350, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 350, 556, 350, 350, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 350, 389, 556, 250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444],
+
+ 'timesBI'=>[250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 350, 611, 350, 350, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 389, 611, 250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333, 400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444],
+
+ 'symbol'=>[250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444, 549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722, 768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500, 500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549, 549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603, 400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658, 823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713, 768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603, 494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494, 0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0],
+
+ 'zapfdingbats'=>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933, 911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537, 577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831, 923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826, 815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762, 762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0, 390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834, 873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874, 0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0]
+ }
+
+ def initialize(orientation='P', unit='mm', format='A4')
+ # Initialization of properties
+ @page=0
+ @n=2
+ @buffer=''
+ @pages=[]
+ @OrientationChanges=[]
+ @state=0
+ @fonts={}
+ @FontFiles={}
+ @diffs=[]
+ @images={}
+ @links=[]
+ @PageLinks={}
+ @InFooter=false
+ @FontFamily=''
+ @FontStyle=''
+ @FontSizePt=12
+ @underline= false
+ @DrawColor='0 G'
+ @FillColor='0 g'
+ @TextColor='0 g'
+ @ColorFlag=false
+ @ws=0
+ @offsets=[]
+
+ # Standard fonts
+ @CoreFonts={}
+ @CoreFonts['courier']='Courier'
+ @CoreFonts['courierB']='Courier-Bold'
+ @CoreFonts['courierI']='Courier-Oblique'
+ @CoreFonts['courierBI']='Courier-BoldOblique'
+ @CoreFonts['helvetica']='Helvetica'
+ @CoreFonts['helveticaB']='Helvetica-Bold'
+ @CoreFonts['helveticaI']='Helvetica-Oblique'
+ @CoreFonts['helveticaBI']='Helvetica-BoldOblique'
+ @CoreFonts['times']='Times-Roman'
+ @CoreFonts['timesB']='Times-Bold'
+ @CoreFonts['timesI']='Times-Italic'
+ @CoreFonts['timesBI']='Times-BoldItalic'
+ @CoreFonts['symbol']='Symbol'
+ @CoreFonts['zapfdingbats']='ZapfDingbats'
+
+ # Scale factor
+ if unit=='pt'
+ @k=1
+ elsif unit=='mm'
+ @k=72/25.4
+ elsif unit=='cm'
+ @k=72/2.54;
+ elsif unit=='in'
+ @k=72
+ else
+ raise 'Incorrect unit: '+unit
+ end
+
+ # Page format
+ if format.is_a? String
+ format.downcase!
+ if format=='a3'
+ format=[841.89,1190.55]
+ elsif format=='a4'
+ format=[595.28,841.89]
+ elsif format=='a5'
+ format=[420.94,595.28]
+ elsif format=='letter'
+ format=[612,792]
+ elsif format=='legal'
+ format=[612,1008]
+ else
+ raise 'Unknown page format: '+format
+ end
+ @fwPt,@fhPt=format
+ else
+ @fwPt=format[0]*@k
+ @fhPt=format[1]*@k
+ end
+ @fw=@fwPt/@k;
+ @fh=@fhPt/@k;
+
+ # Page orientation
+ orientation.downcase!
+ if orientation=='p' or orientation=='portrait'
+ @DefOrientation='P'
+ @wPt=@fwPt
+ @hPt=@fhPt
+ elsif orientation=='l' or orientation=='landscape'
+ @DefOrientation='L'
+ @wPt=@fhPt
+ @hPt=@fwPt
+ else
+ raise 'Incorrect orientation: '+orientation
+ end
+ @CurOrientation=@DefOrientation
+ @w=@wPt/@k
+ @h=@hPt/@k
+
+ # Page margins (1 cm)
+ margin=28.35/@k
+ SetMargins(margin,margin)
+ # Interior cell margin (1 mm)
+ @cMargin=margin/10
+ # Line width (0.2 mm)
+ @LineWidth=0.567/@k
+ # Automatic page break
+ SetAutoPageBreak(true,2*margin)
+ # Full width display mode
+ SetDisplayMode('fullwidth')
+ # Enable compression
+ SetCompression(true)
+ # Set default PDF version number
+ @PDFVersion='1.3'
+ end
+
+ def SetMargins(left, top, right=-1)
+ # Set left, top and right margins
+ @lMargin=left
+ @tMargin=top
+ right=left if right==-1
+ @rMargin=right
+ end
+
+ def SetLeftMargin(margin)
+ # Set left margin
+ @lMargin=margin
+ @x=margin if @page>0 and @x<margin
+ end
+
+ def SetTopMargin(margin)
+ # Set top margin
+ @tMargin=margin
+ end
+
+ def SetRightMargin(margin)
+ #Set right margin
+ @rMargin=margin
+ end
+
+ def SetAutoPageBreak(auto, margin=0)
+ # Set auto page break mode and triggering margin
+ @AutoPageBreak=auto
+ @bMargin=margin
+ @PageBreakTrigger=@h-margin
+ end
+
+ def SetDisplayMode(zoom, layout='continuous')
+ # Set display mode in viewer
+ if zoom=='fullpage' or zoom=='fullwidth' or zoom=='real' or
+ zoom=='default' or not zoom.kind_of? String
+
+ @ZoomMode=zoom;
+ elsif zoom=='zoom'
+ @ZoomMode=layout
+ else
+ raise 'Incorrect zoom display mode: '+zoom
+ end
+ if layout=='single' or layout=='continuous' or layout=='two' or
+ layout=='default'
+
+ @LayoutMode=layout
+ elsif zoom!='zoom'
+ raise 'Incorrect layout display mode: '+layout
+ end
+ end
+
+ def SetCompression(compress)
+ # Set page compression
+ @compress = compress
+ end
+
+ def SetTitle(title)
+ # Title of document
+ @title=title
+ end
+
+ def SetSubject(subject)
+ # Subject of document
+ @subject=subject
+ end
+
+ def SetAuthor(author)
+ # Author of document
+ @author=author
+ end
+
+ def SetKeywords(keywords)
+ # Keywords of document
+ @keywords=keywords
+ end
+
+ def SetCreator(creator)
+ # Creator of document
+ @creator=creator
+ end
+
+ def AliasNbPages(aliasnb='{nb}')
+ # Define an alias for total number of pages
+ @AliasNbPages=aliasnb
+ end
+
+ def Error(msg)
+ raise 'FPDF error: '+msg
+ end
+
+ def Open
+ # Begin document
+ @state=1
+ end
+
+ def Close
+ # Terminate document
+ return if @state==3
+ self.AddPage if @page==0
+ # Page footer
+ @InFooter=true
+ self.Footer
+ @InFooter=false
+ # Close page
+ endpage
+ # Close document
+ enddoc
+ end
+
+ def AddPage(orientation='')
+ # Start a new page
+ self.Open if @state==0
+ family=@FontFamily
+ style=@FontStyle+(@underline ? 'U' : '')
+ size=@FontSizePt
+ lw=@LineWidth
+ dc=@DrawColor
+ fc=@FillColor
+ tc=@TextColor
+ cf=@ColorFlag
+ if @page>0
+ # Page footer
+ @InFooter=true
+ self.Footer
+ @InFooter=false
+ # Close page
+ endpage
+ end
+ # Start new page
+ beginpage(orientation)
+ # Set line cap style to square
+ out('2 J')
+ # Set line width
+ @LineWidth=lw
+ out(sprintf('%.2f w',lw*@k))
+ # Set font
+ SetFont(family,style,size) if family
+ # Set colors
+ @DrawColor=dc
+ out(dc) if dc!='0 G'
+ @FillColor=fc
+ out(fc) if fc!='0 g'
+ @TextColor=tc
+ @ColorFlag=cf
+ # Page header
+ self.Header
+ # Restore line width
+ if @LineWidth!=lw
+ @LineWidth=lw
+ out(sprintf('%.2f w',lw*@k))
+ end
+ # Restore font
+ self.SetFont(family,style,size) if family
+ # Restore colors
+ if @DrawColor!=dc
+ @DrawColor=dc
+ out(dc)
+ end
+ if @FillColor!=fc
+ @FillColor=fc
+ out(fc)
+ end
+ @TextColor=tc
+ @ColorFlag=cf
+ end
+
+ def Header
+ # To be implemented in your inherited class
+ end
+
+ def Footer
+ # To be implemented in your inherited class
+ end
+
+ def PageNo
+ # Get current page number
+ @page
+ end
+
+ 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
+ @DrawColor=sprintf('%.3f G',r/255.0)
+ else
+ @DrawColor=sprintf('%.3f %.3f %.3f RG',r/255.0,g/255.0,b/255.0)
+ end
+ out(@DrawColor) if(@page>0)
+ end
+
+ def SetFillColor(r,g=-1,b=-1)
+ # Set color for all filling operations
+ if (r==0 and g==0 and b==0) or g==-1
+ @FillColor=sprintf('%.3f g',r/255.0)
+ else
+ @FillColor=sprintf('%.3f %.3f %.3f rg',r/255.0,g/255.0,b/255.0)
+ end
+ @ColorFlag=(@FillColor!=@TextColor)
+ out(@FillColor) if(@page>0)
+ end
+
+ def SetTextColor(r,g=-1,b=-1)
+ # Set color for text
+ if (r==0 and g==0 and b==0) or g==-1
+ @TextColor=sprintf('%.3f g',r/255.0)
+ else
+ @TextColor=sprintf('%.3f %.3f %.3f rg',r/255.0,g/255.0,b/255.0)
+ end
+ @ColorFlag=(@FillColor!=@TextColor)
+ end
+
+ def GetStringWidth(s)
+ # Get width of a string in the current font
+ cw=@CurrentFont['cw']
+ w=0
+ s.each_byte do |c|
+ w=w+cw[c]
+ end
+ w*@FontSize/1000.0
+ end
+
+ def SetLineWidth(width)
+ # Set line width
+ @LineWidth=width
+ out(sprintf('%.2f w',width*@k)) if @page>0
+ end
+
+ 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
+
+ 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
+
+ def AddFont(family, style='', file='')
+ # Add a TrueType or Type1 font
+ family = family.downcase
+ family = 'helvetica' if family == 'arial'
+
+ style = style.upcase
+ style = 'BI' if style == 'IB'
+
+ fontkey = family + style
+
+ if @fonts.has_key?(fontkey)
+ self.Error("Font already added: #{family} #{style}")
+ end
+
+ file = family.gsub(' ', '') + style.downcase + '.rb' if file == ''
+
+ if self.class.const_defined? 'FPDF_FONTPATH'
+ if FPDF_FONTPATH[-1,1] == '/'
+ file = FPDF_FONTPATH + file
+ else
+ file = FPDF_FONTPATH + '/' + file
+ end
+ end
+
+ # Changed from "require file" to fix bug reported by Hans Allis.
+ load file
+
+ if FontDef.desc.nil?
+ self.Error("Could not include font definition file #{file}")
+ end
+
+ i = @fonts.length + 1
+
+ @fonts[fontkey] = {'i' => i,
+ 'type' => FontDef.type,
+ 'name' => FontDef.name,
+ 'desc' => FontDef.desc,
+ 'up' => FontDef.up,
+ 'ut' => FontDef.ut,
+ 'cw' => FontDef.cw,
+ 'enc' => FontDef.enc,
+ 'file' => FontDef.file
+ }
+
+ if FontDef.diff
+ # Search existing encodings
+ unless @diffs.include?(FontDef.diff)
+ @diffs.push(FontDef.diff)
+ @fonts[fontkey]['diff'] = @diffs.length - 1
+ end
+ end
+
+ if FontDef.file
+ if FontDef.type == 'TrueType'
+ @FontFiles[FontDef.file] = {'length1' => FontDef.originalsize}
+ else
+ @FontFiles[FontDef.file] = {'length1' => FontDef.size1, 'length2' => FontDef.size2}
+ end
+ end
+
+ return self
+ end
+
+ def SetFont(family, style='', size=0)
+ # Select a font; size given in points
+ family.downcase!
+ family=@FontFamily if family==''
+ if family=='arial'
+ family='helvetica'
+ elsif family=='symbol' or family=='zapfdingbats'
+ style=''
+ end
+ style.upcase!
+ unless style.index('U').nil?
+ @underline=true
+ style.gsub!('U','')
+ else
+ @underline=false;
+ end
+ style='BI' if style=='IB'
+ size=@FontSizePt if size==0
+ # Test if font is already selected
+ return if @FontFamily==family and
+ @FontStyle==style and @FontSizePt==size
+ # Test if used for the first time
+ fontkey=family+style
+ unless @fonts.has_key?(fontkey)
+ if @CoreFonts.has_key?(fontkey)
+ unless Charwidths.has_key?(fontkey)
+ raise 'Font unavailable'
+ end
+ @fonts[fontkey]={
+ 'i'=>@fonts.size,
+ 'type'=>'core',
+ 'name'=>@CoreFonts[fontkey],
+ 'up'=>-100,
+ 'ut'=>50,
+ 'cw'=>Charwidths[fontkey]}
+ else
+ raise 'Font unavailable'
+ end
+ end
+
+ #Select it
+ @FontFamily=family
+ @FontStyle=style;
+ @FontSizePt=size
+ @FontSize=size/@k;
+ @CurrentFont=@fonts[fontkey]
+ if @page>0
+ out(sprintf('BT /F%d %.2f Tf ET', @CurrentFont['i'], @FontSizePt))
+ end
+ end
+
+ def SetFontSize(size)
+ # Set font size in points
+ return if @FontSizePt==size
+ @FontSizePt=size
+ @FontSize=size/@k
+ if @page>0
+ out(sprintf('BT /F%d %.2f Tf ET',@CurrentFont['i'],@FontSizePt))
+ end
+ end
+
+ def AddLink
+ # Create a new internal link
+ @links.push([0, 0])
+ @links.size
+ end
+
+ def SetLink(link, y=0, page=-1)
+ # Set destination of internal link
+ y=@y if y==-1
+ page=@page if page==-1
+ @links[link]=[page, y]
+ end
+
+ def Link(x, y, w, h, link)
+ # Put a link on the page
+ @PageLinks[@page]=Array.new unless @PageLinks.has_key?(@Page)
+ @PageLinks[@page].push([x*@k,@hPt-y*@k,w*@k,h*@k,link])
+ end
+
+ def Text(x, y, txt)
+ # Output a string
+ txt.gsub!(')', '\\)')
+ txt.gsub!('(', '\\(')
+ txt.gsub!('\\', '\\\\')
+ s=sprintf('BT %.2f %.2f Td (%s) Tj ET',x*@k,(@h-y)*@k,txt);
+ s=s+' '+dounderline(x,y,txt) if @underline and txt!=''
+ s='q '+@TextColor+' '+s+' Q' if @ColorFlag
+ out(s)
+ end
+
+ def AcceptPageBreak
+ # Accept automatic page break or not
+ @AutoPageBreak
+ end
+
+ def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
+ # Output a cell
+ if @y+h>@PageBreakTrigger and !@InFooter and self.AcceptPageBreak
+ # Automatic page break
+ x=@x
+ ws=@ws
+ if ws>0
+ @ws=0
+ out('0 Tw')
+ end
+ self.AddPage(@CurOrientation)
+ @x=x
+ if ws>0
+ @ws=ws
+ out(sprintf('%.3f Tw',ws*@k))
+ end
+ end
+ w=@w-@rMargin-@x if w==0
+ s=''
+ if fill==1 or border==1
+ if fill==1
+ op=(border==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
+ unless border.index('L').nil?
+ s=s+sprintf('%.2f %.2f m %.2f %.2f l S ',
+ x*@k,(@h-y)*@k,x*@k,(@h-(y+h))*@k)
+ end
+ unless border.index('T').nil?
+ s=s+sprintf('%.2f %.2f m %.2f %.2f l S ',
+ x*@k,(@h-y)*@k,(x+w)*@k,(@h-y)*@k)
+ end
+ unless border.index('R').nil?
+ s=s+sprintf('%.2f %.2f m %.2f %.2f l S ',
+ (x+w)*@k,(@h-y)*@k,(x+w)*@k,(@h-(y+h))*@k)
+ end
+ unless border.index('B').nil?
+ s=s+sprintf('%.2f %.2f m %.2f %.2f l S ',
+ x*@k,(@h-(y+h))*@k,(x+w)*@k,(@h-(y+h))*@k)
+ end
+ end
+ if txt!=''
+ if align=='R'
+ dx=w-@cMargin-self.GetStringWidth(txt)
+ elsif align=='C'
+ dx=(w-self.GetStringWidth(txt))/2
+ else
+ dx=@cMargin
+ end
+ txt = txt.gsub(')', '\\)')
+ txt.gsub!('(', '\\(')
+ txt.gsub!('\\', '\\\\')
+ if @ColorFlag
+ s=s+'q '+@TextColor+' '
+ end
+ s=s+sprintf('BT %.2f %.2f Td (%s) Tj ET',
+ (@x+dx)*@k,(@h-(@y+0.5*h+0.3*@FontSize))*@k,txt)
+ s=s+' '+dounderline(@x+dx,@y+0.5*h+0.3*@FontSize,txt) if @underline
+ s=s+' Q' if @ColorFlag
+ if link and link != ''
+ Link(@x+dx,@y+0.5*h-0.5*@FontSize,GetStringWidth(txt),@FontSize,link)
+ end
+ end
+ out(s) if s
+ @lasth=h
+ if ln>0
+ # Go to next line
+ @y=@y+h
+ @x=@lMargin if ln==1
+ else
+ @x=@x+w
+ end
+ end
+
+ def MultiCell(w,h,txt,border=0,align='J',fill=0)
+ # Output text with automatic or explicit line breaks
+ cw=@CurrentFont['cw']
+ w=@w-@rMargin-@x if w==0
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub('\r','')
+ nb=s.length
+ nb=nb-1 if nb>0 and s[nb-1].chr=='\n'
+ b=0
+ if border!=0
+ if border==1
+ border='LTRB'
+ b='LRT'
+ b2='LR'
+ else
+ b2=''
+ b2='L' unless border.index('L').nil?
+ b2=b2+'R' unless border.index('R').nil?
+ b=(not border.index('T').nil?) ? (b2+'T') : b2
+ end
+ end
+ sep=-1
+ i=0
+ j=0
+ l=0
+ ns=0
+ nl=1
+ while i<nb
+ # Get next character
+ c=s[i].chr
+ if c=="\n"
+ # Explicit line break
+ if @ws>0
+ @ws=0
+ out('0 Tw')
+ end
+#Ed Moss
+# Don't let i go negative
+ end_i = i == 0 ? 0 : i - 1
+ # Changed from s[j..i] to fix bug reported by Hans Allis.
+ self.Cell(w,h,s[j..end_i],b,2,align,fill)
+#
+ i=i+1
+ sep=-1
+ j=i
+ l=0
+ ns=0
+ nl=nl+1
+ b=b2 if border and nl==2
+ else
+ if c==' '
+ sep=i
+ ls=l
+ ns=ns+1
+ end
+ l=l+cw[c[0]]
+ if l>wmax
+ # Automatic line break
+ if sep==-1
+ i=i+1 if i==j
+ if @ws>0
+ @ws=0
+ out('0 Tw')
+ end
+ self.Cell(w,h,s[j..i],b,2,align,fill)
+#Ed Moss
+# Added so that it wouldn't print the last character of the string if it got close
+#FIXME 2006-07-18 Level=0 - but it still puts out an extra new line
+ i += 1
+#
+ else
+ if align=='J'
+ @ws=(ns>1) ? (wmax-ls)/1000.0*@FontSize/(ns-1) : 0
+ out(sprintf('%.3f Tw',@ws*@k))
+ end
+ self.Cell(w,h,s[j..sep],b,2,align,fill)
+ i=sep+1
+ end
+ sep=-1
+ j=i
+ l=0
+ ns=0
+ nl=nl+1
+ b=b2 if border and nl==2
+ else
+ i=i+1
+ end
+ end
+ end
+
+ # Last chunk
+ if @ws>0
+ @ws=0
+ out('0 Tw')
+ end
+ b=b+'B' if border!=0 and not border.index('B').nil?
+ self.Cell(w,h,s[j..i],b,2,align,fill)
+ @x=@lMargin
+ end
+
+ def Write(h,txt,link='')
+ # Output text in flowing mode
+ cw=@CurrentFont['cw']
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while i<nb
+ # Get next character
+ c=s[i]
+ if c=="\n"[0]
+ # Explicit line break
+ self.Cell(w,h,s[j,i-j],0,2,'',0,link)
+ i=i+1
+ sep=-1
+ j=i
+ l=0
+ if nl==1
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl=nl+1
+ next
+ end
+ if c==' '[0]
+ sep=i
+ ls=l
+ end
+ l=l+cw[c];
+ if l>wmax
+ # Automatic line break
+ if sep==-1
+ if @x>@lMargin
+ # Move to next line
+ @x=@lMargin
+ @y=@y+h
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ i=i+1
+ nl=nl+1
+ next
+ end
+ i=i+1 if i==j
+ self.Cell(w,h,s[j,i-j],0,2,'',0,link)
+ else
+ self.Cell(w,h,s[j,sep-j],0,2,'',0,link)
+ i=sep+1
+ end
+ sep=-1
+ j=i
+ l=0
+ if nl==1
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl=nl+1
+ else
+ i=i+1
+ end
+ end
+ # Last chunk
+ self.Cell(l/1000.0*@FontSize,h,s[j,i],0,0,'',0,link) if i!=j
+ end
+
+ def Image(file,x,y,w=0,h=0,type='',link='')
+ # Put an image on the page
+ unless @images.has_key?(file)
+ # First use of image, get info
+ if type==''
+ pos=file.rindex('.')
+ if pos.nil?
+ self.Error('Image file has no extension and no type was '+
+ 'specified: '+file)
+ end
+ type=file[pos+1..-1]
+ end
+ type.downcase!
+ if type=='jpg' or type=='jpeg'
+ info=parsejpg(file)
+ elsif type=='png'
+ info=parsepng(file)
+ else
+ self.Error('Unsupported image file type: '+type)
+ end
+ info['i']=@images.length+1
+ @images[file]=info
+ else
+ info=@images[file]
+ end
+#Ed Moss
+ if(w==0 && h==0)
+ #Put image at 72 dpi
+ w=info['w']/@k;
+ h=info['h']/@k;
+ end
+#
+ # Automatic width or height calculation
+ w=h*info['w']/info['h'] if w==0
+ h=w*info['h']/info['w'] if h==0
+ 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']))
+ Link(x,y,w,h,link) if link and link != ''
+ end
+
+ def Ln(h='')
+ # Line feed; default value is last cell height
+ @x=@lMargin
+ if h.kind_of?(String)
+ @y=@y+@lasth
+ else
+ @y=@y+h
+ end
+ end
+
+ def GetX
+ # Get x position
+ @x
+ end
+
+ def SetX(x)
+ # Set x position
+ if x>=0
+ @x=x
+ else
+ @x=@w+x
+ end
+ end
+
+ def GetY
+ # Get y position
+ @y
+ end
+
+ def SetY(y)
+ # Set y position and reset x
+ @x=@lMargin
+ if y>=0
+ @y=y
+ else
+ @y=@h+y
+ end
+ end
+
+ def SetXY(x,y)
+ # Set x and y positions
+ SetY(y)
+ SetX(x)
+ end
+
+ def Output(file=nil)
+ # Output PDF to file or return as a string
+
+ # Finish document if necessary
+ self.Close if(@state<3)
+
+ if file.nil?
+ # Return as a string
+ return @buffer
+ else
+ # Save file locally
+ open(file,'wb') do |f|
+ f.write(@buffer)
+ end
+ end
+ end
+
+ private
+
+ def putpages
+ nb=@page
+ unless @AliasNbPages.nil? or @AliasNbPages==''
+ # Replace number of pages
+ 1.upto(nb) do |n|
+ @pages[n].gsub!(@AliasNbPages,nb.to_s)
+ end
+ end
+ if @DefOrientation=='P'
+ wPt=@fwPt
+ hPt=@fhPt
+ else
+ wPt=@fhPt
+ hPt=@fwPt
+ end
+ filter=(@compress) ? '/Filter /FlateDecode ' : ''
+ 1.upto(nb) do |n|
+ # Page
+ newobj
+ out('<</Type /Page')
+ out('/Parent 1 0 R')
+ unless @OrientationChanges[n].nil?
+ out(sprintf('/MediaBox [0 0 %.2f %.2f]',hPt,wPt))
+ end
+ out('/Resources 2 0 R')
+ if @PageLinks[n]
+ # Links
+ annots='/Annots ['
+ @PageLinks[n].each do |pl|
+ rect=sprintf('%.2f %.2f %.2f %.2f',
+ pl[0],pl[1],pl[0]+pl[2],pl[1]-pl[3])
+ annots=annots+'<</Type /Annot /Subtype /Link /Rect ['+rect+
+ '] /Border [0 0 0] '
+ if pl[4].kind_of?(String)
+ annots=annots+'/A <</S /URI /URI '+textstring(pl[4])+
+ '>>>>'
+ else
+ l=@links[pl[4]]
+ h=@OrientationChanges[l[0]].nil? ? hPt : wPt
+ annots=annots+sprintf(
+ '/Dest [%d 0 R /XYZ 0 %.2f null]>>',
+ 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) ? Zlib::Deflate.deflate(@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('<</Type /Pages')
+ kids='/Kids ['
+ nb.times do |i|
+ kids=kids+(3+2*i).to_s+' 0 R '
+ end
+ out(kids+']')
+ out('/Count '+nb.to_s)
+ out(sprintf('/MediaBox [0 0 %.2f %.2f]',wPt,hPt))
+ out('>>')
+ out('endobj')
+ end
+
+ def putfonts
+ nf=@n
+ @diffs.each do |diff|
+ # Encodings
+ newobj
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences '+
+ '['+diff+']>>')
+ out('endobj')
+ end
+
+ @FontFiles.each do |file, info|
+ # Font file embedding
+ newobj
+ @FontFiles[file]['n'] = @n
+
+ if self.class.const_defined? 'FPDF_FONTPATH' then
+ if FPDF_FONTPATH[-1,1] == '/' then
+ file = FPDF_FONTPATH + file
+ else
+ file = FPDF_FONTPATH + '/' + file
+ end
+ end
+
+ size = File.size(file)
+ unless File.exists?(file)
+ Error('Font file not found')
+ end
+
+ out('<</Length ' + size.to_s)
+
+ if file[-2, 2] == '.z' then
+ out('/Filter /FlateDecode')
+ end
+ out('/Length1 ' + info['length1'])
+ out('/Length2 ' + info['length2'] + ' /Length3 0') if info['length2']
+ out('>>')
+ open(file, 'rb') do |f|
+ putstream(f.read())
+ end
+ out('endobj')
+ end
+
+ file = 0
+ @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('<</Type /Font')
+ out('/BaseFont /'+name)
+ out('/Subtype /Type1')
+ if name!='Symbol' and name!='ZapfDingbats'
+ out('/Encoding /WinAnsiEncoding')
+ end
+ out('>>')
+ out('endobj')
+ elsif type=='Type1' or type=='TrueType'
+ # Additional Type1 or TrueType font
+ newobj
+ out('<</Type /Font')
+ out('/BaseFont /'+name)
+ out('/Subtype /'+type)
+ out('/FirstChar 32 /LastChar 255')
+ out('/Widths '+(@n+1).to_s+' 0 R')
+ out('/FontDescriptor '+(@n+2).to_s+' 0 R')
+ if font['enc'] and font['enc'] != ''
+ unless font['diff'].nil?
+ out('/Encoding '+(nf+font['diff']).to_s+' 0 R')
+ else
+ out('/Encoding /WinAnsiEncoding')
+ end
+ end
+ out('>>')
+ out('endobj')
+ # Widths
+ newobj
+ cw=font['cw']
+ s='['
+ 32.upto(255) do |i|
+ s << cw[i].to_s+' '
+ end
+ out(s+']')
+ out('endobj')
+ # Descriptor
+ newobj
+ s='<</Type /FontDescriptor /FontName /'+name
+ font['desc'].each do |k, v|
+ s << ' /'+k+' '+v
+ end
+ file=font['file']
+ if file
+ s << ' /FontFile'+(type=='Type1' ? '' : '2')+' '+
+ @FontFiles[file]['n'].to_s+' 0 R'
+ end
+ out(s+'>>')
+ out('endobj')
+ else
+ # Allow for additional types
+ mtd='put'+type.downcase
+ unless self.respond_to?(mtd)
+ self.Error('Unsupported font type: '+type)
+ end
+ self.send(mtd, font)
+ end
+ end
+ end
+
+ def putimages
+ filter=(@compress) ? '/Filter /FlateDecode ' : ''
+ @images.each do |file, info|
+ newobj
+ @images[file]['n']=@n
+ out('<</Type /XObject')
+ out('/Subtype /Image')
+ out('/Width '+info['w'].to_s)
+ out('/Height '+info['h'].to_s)
+ if info['cs']=='Indexed'
+ out("/ColorSpace [/Indexed /DeviceRGB #{info['pal'].length/3-1} #{(@n+1)} 0 R]")
+ else
+ out('/ColorSpace /'+info['cs'])
+ if info['cs']=='DeviceCMYK'
+ out('/Decode [1 0 1 0 1 0 1 0]')
+ end
+ end
+ out('/BitsPerComponent '+info['bpc'].to_s)
+ out('/Filter /'+info['f']) if info['f']
+ unless info['parms'].nil?
+ out(info['parms'])
+ end
+ if info['trns'] and info['trns'].kind_of?(Array)
+ trns=''
+ info['trns'].length.times do |i|
+ trns=trns+info['trns'][i].to_s+' '+info['trns'][i].to_s+' '
+ end
+ out('/Mask ['+trns+']')
+ end
+ out('/Length '+info['data'].length.to_s+'>>')
+ putstream(info['data'])
+ @images[file]['data']=nil
+ out('endobj')
+ # Palette
+ if info['cs']=='Indexed'
+ newobj
+ pal=(@compress) ? Zlib::Deflate.deflate(info['pal']) : info['pal']
+ out('<<'+filter+'/Length '+pal.length.to_s+'>>')
+ putstream(pal)
+ out('endobj')
+ end
+ end
+ end
+
+ def putxobjectdict
+ @images.each_value do |image|
+ out('/I'+image['i'].to_s+' '+image['n'].to_s+' 0 R')
+ end
+ end
+
+ 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
+
+ def putresources
+ putfonts
+ putimages
+ # Resource dictionary
+ @offsets[2]=@buffer.length
+ out('2 0 obj')
+ out('<<')
+ putresourcedict
+ out('>>')
+ out('endobj')
+ end
+
+ def putinfo
+ out('/Producer '+textstring('Ruby FPDF '+FPDF_VERSION));
+ unless @title.nil?
+ out('/Title '+textstring(@title))
+ end
+ unless @subject.nil?
+ out('/Subject '+textstring(@subject))
+ end
+ unless @author.nil?
+ out('/Author '+textstring(@author))
+ end
+ unless @keywords.nil?
+ out('/Keywords '+textstring(@keywords))
+ end
+ unless @creator.nil?
+ out('/Creator '+textstring(@creator))
+ end
+ out('/CreationDate '+textstring('D: '+DateTime.now.to_s))
+ end
+
+ def putcatalog
+ out('/Type /Catalog')
+ out('/Pages 1 0 R')
+ if @ZoomMode=='fullpage'
+ out('/OpenAction [3 0 R /Fit]')
+ elsif @ZoomMode=='fullwidth'
+ out('/OpenAction [3 0 R /FitH null]')
+ elsif @ZoomMode=='real'
+ out('/OpenAction [3 0 R /XYZ null null 1]')
+ elsif not @ZoomMode.kind_of?(String)
+ out('/OpenAction [3 0 R /XYZ null null '+(@ZoomMode/100)+']')
+ end
+
+ if @LayoutMode=='single'
+ out('/PageLayout /SinglePage')
+ elsif @LayoutMode=='continuous'
+ out('/PageLayout /OneColumn')
+ elsif @LayoutMode=='two'
+ out('/PageLayout /TwoColumnLeft')
+ end
+ end
+
+ def putheader
+ out('%PDF-'+@PDFVersion)
+ end
+
+ def puttrailer
+ out('/Size '+(@n+1).to_s)
+ out('/Root '+@n.to_s+' 0 R')
+ out('/Info '+(@n-1).to_s+' 0 R')
+ end
+
+ 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
+
+ def beginpage(orientation)
+ @page=@page+1
+ @pages[@page]=''
+ @state=2
+ @x=@lMargin
+ @y=@tMargin
+ @lasth=0
+ @FontFamily=''
+ # Page orientation
+ if orientation==''
+ orientation=@DefOrientation
+ else
+ orientation=orientation[0].chr.upcase
+ if orientation!=@DefOrientation
+ @OrientationChanges[@page]=true
+ end
+ end
+ if orientation!=@CurOrientation
+ # Change orientation
+ if orientation=='P'
+ @wPt=@fwPt
+ @hPt=@fhPt
+ @w=@fw
+ @h=@fh
+ else
+ @wPt=@fhPt
+ @hPt=@fwPt
+ @w=@fh
+ @h=@fw
+ end
+ @PageBreakTrigger=@h-@bMargin
+ @CurOrientation=orientation
+ end
+ end
+
+ def endpage
+ # End of page contents
+ @state=1
+ end
+
+ def newobj
+ # Begin a new object
+ @n=@n+1
+ @offsets[@n]=@buffer.length
+ out(@n.to_s+' 0 obj')
+ end
+
+ def dounderline(x,y,txt)
+ # Underline text
+ up=@CurrentFont['up']
+ ut=@CurrentFont['ut']
+ w=GetStringWidth(txt)+@ws*txt.count(' ')
+ sprintf('%.2f %.2f %.2f %.2f re f',
+ x*@k,(@h-(y-up/1000.0*@FontSize))*@k,w*@k,-ut/1000.0*@FontSizePt)
+ end
+
+ def parsejpg(file)
+ # Extract info from a JPEG file
+ a=extractjpginfo(file)
+ raise "Missing or incorrect JPEG file: #{file}" if a.nil?
+
+ if a['channels'].nil? || a['channels']==3 then
+ colspace='DeviceRGB'
+ elsif a['channels']==4 then
+ colspace='DeviceCMYK'
+ else
+ colspace='DeviceGray'
+ end
+ bpc= a['bits'] ? a['bits'].to_i : 8
+
+ # Read whole file
+ data = nil
+ open(file, 'rb') do |f|
+ data = f.read
+ end
+ return {'w'=>a['width'],'h'=>a['height'],'cs'=>colspace,'bpc'=>bpc,'f'=>'DCTDecode','data'=>data}
+ end
+
+ def parsepng(file)
+ # Extract info from a PNG file
+ f=open(file,'rb')
+ # Check signature
+ unless f.read(8)==137.chr+'PNG'+13.chr+10.chr+26.chr+10.chr
+ self.Error('Not a PNG file: '+file)
+ end
+ # Read header chunk
+ f.read(4)
+ if f.read(4)!='IHDR'
+ self.Error('Incorrect PNG file: '+file)
+ end
+ w=freadint(f)
+ h=freadint(f)
+ bpc=f.read(1)[0]
+ if bpc>8
+ self.Error('16-bit depth not supported: '+file)
+ end
+ ct=f.read(1)[0]
+ if ct==0
+ colspace='DeviceGray'
+ elsif ct==2
+ colspace='DeviceRGB'
+ elsif ct==3
+ colspace='Indexed'
+ else
+ self.Error('Alpha channel not supported: '+file)
+ end
+ if f.read(1)[0]!=0
+ self.Error('Unknown compression method: '+file)
+ end
+ if f.read(1)[0]!=0
+ self.Error('Unknown filter method: '+file)
+ end
+ if f.read(1)[0]!=0
+ self.Error('Interlacing not supported: '+file)
+ end
+ f.read(4)
+ parms='/DecodeParms <</Predictor 15 /Colors '+(ct==2 ? '3' : '1')+
+ ' /BitsPerComponent '+bpc.to_s+' /Columns '+w.to_s+'>>'
+ # 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]]
+ elsif ct==2
+ trns=[t[1],t[3],t[5]]
+ else
+ pos=t.index(0)
+ trns=[pos] unless pos.nil?
+ end
+ f.read(4)
+ elsif type=='IDAT'
+ # Read image data block
+ data << f.read(n)
+ f.read(4)
+ elsif type=='IEND'
+ break
+ else
+ f.read(n+4)
+ end
+ end while n
+ if colspace=='Indexed' and pal==''
+ self.Error('Missing palette in '+file)
+ end
+ f.close
+ {'w'=>w,'h'=>h,'cs'=>colspace,'bpc'=>bpc,'f'=>'FlateDecode',
+ 'parms'=>parms,'pal'=>pal,'trns'=>trns,'data'=>data}
+ end
+
+ def freadint(f)
+ # Read a 4-byte integer from file
+ a = f.read(4).unpack('N')
+ return a[0]
+ end
+
+ def freadshort(f)
+ a = f.read(2).unpack('n')
+ return a[0]
+ end
+
+ def freadbyte(f)
+ a = f.read(1).unpack('C')
+ return a[0]
+ end
+
+ def textstring(s)
+ # Format a text string
+ '('+escape(s)+')'
+ end
+
+ def escape(s)
+ # Add \ before \, ( and )
+ s.gsub('\\','\\\\').gsub('(','\\(').gsub(')','\\)')
+ end
+
+ def putstream(s)
+ out('stream')
+ out(s)
+ out('endstream')
+ end
+
+ def out(s)
+ # Add a line to the document
+ if @state==2
+ @pages[@page]=@pages[@page]+s+"\n"
+ else
+ @buffer=@buffer+s.to_s+"\n"
+ end
+ end
+
+ # jpeg marker codes
+
+ M_SOF0 = 0xc0
+ M_SOF1 = 0xc1
+ M_SOF2 = 0xc2
+ M_SOF3 = 0xc3
+
+ M_SOF5 = 0xc5
+ M_SOF6 = 0xc6
+ M_SOF7 = 0xc7
+
+ M_SOF9 = 0xc9
+ M_SOF10 = 0xca
+ M_SOF11 = 0xcb
+
+ M_SOF13 = 0xcd
+ M_SOF14 = 0xce
+ M_SOF15 = 0xcf
+
+ M_SOI = 0xd8
+ M_EOI = 0xd9
+ M_SOS = 0xda
+
+ def extractjpginfo(file)
+ result = nil
+
+ open(file, "rb") do |f|
+ marker = jpegnextmarker(f)
+
+ if marker != M_SOI
+ return nil
+ end
+
+ while true
+ marker = jpegnextmarker(f)
+
+ case marker
+ when M_SOF0, M_SOF1, M_SOF2, M_SOF3,
+ M_SOF5, M_SOF6, M_SOF7, M_SOF9,
+ M_SOF10, M_SOF11, M_SOF13, M_SOF14,
+ M_SOF15 then
+
+ length = freadshort(f)
+
+ if result.nil?
+ result = {}
+
+ result['bits'] = freadbyte(f)
+ result['height'] = freadshort(f)
+ result['width'] = freadshort(f)
+ result['channels'] = freadbyte(f)
+
+ f.seek(length - 8, IO::SEEK_CUR)
+ else
+ f.seek(length - 2, IO::SEEK_CUR)
+ end
+ when M_SOS, M_EOI then
+ return result
+ else
+ length = freadshort(f)
+ f.seek(length - 2, IO::SEEK_CUR)
+ end
+ end
+ end
+ end
+
+ def jpegnextmarker(f)
+ while true
+ # look for 0xff
+ while (c = freadbyte(f)) != 0xff
+ end
+
+ c = freadbyte(f)
+
+ if c != 0
+ return c
+ end
+ end
+ end
+end
--- /dev/null
+# Information
+#
+# PDF_EPS class from Valentin Schmidt ported to ruby by Thiago Jackiw (tjackiw@gmail.com)
+# working for Mingle LLC (www.mingle.com)
+# Release Date: July 13th, 2006
+#
+# Description
+#
+# This script allows to embed vector-based Adobe Illustrator (AI) or AI-compatible EPS files.
+# Only vector drawing is supported, not text or bitmap. Although the script was successfully
+# tested with various AI format versions, best results are probably achieved with files that
+# were exported in the AI3 format (tested with Illustrator CS2, Freehand MX and Photoshop CS2).
+#
+# ImageEps(string file, float x, float y [, float w [, float h [, string link [, boolean useBoundingBox]]]])
+#
+# Same parameters as for regular FPDF::Image() method, with an additional one:
+#
+# useBoundingBox: specifies whether to position the bounding box (true) or the complete canvas (false)
+# at location (x,y). Default value is true.
+#
+# First added to the Ruby FPDF distribution in 1.53c
+#
+# Usage is as follows:
+#
+# require 'fpdf'
+# require 'fpdf_eps'
+# pdf = FPDF.new
+# pdf.extend(PDF_EPS)
+# pdf.ImageEps(...)
+#
+# This allows it to be combined with other extensions, such as the bookmark
+# module.
+
+module PDF_EPS
+ def ImageEps(file, x, y, w=0, h=0, link='', use_bounding_box=true)
+ data = nil
+ if File.exists?(file)
+ File.open(file, 'rb') do |f|
+ data = f.read()
+ end
+ else
+ Error('EPS file not found: '+file)
+ end
+
+ # Find BoundingBox param
+ regs = data.scan(/%%BoundingBox: [^\r\n]*/m)
+ regs << regs[0].gsub(/%%BoundingBox: /, '')
+ if regs.size > 1
+ tmp = regs[1].to_s.split(' ')
+ @x1 = tmp[0].to_i
+ @y1 = tmp[1].to_i
+ @x2 = tmp[2].to_i
+ @y2 = tmp[3].to_i
+ else
+ Error('No BoundingBox found in EPS file: '+file)
+ end
+ f_start = data.index('%%EndSetup')
+ f_start = data.index('%%EndProlog') if f_start === false
+ f_start = data.index('%%BoundingBox') if f_start === false
+
+ data = data.slice(f_start, data.length)
+
+ f_end = data.index('%%PageTrailer')
+ f_end = data.index('showpage') if f_end === false
+ data = data.slice(0, f_end) if f_end
+
+ # save the current graphic state
+ out('q')
+
+ k = @k
+
+ # Translate
+ if use_bounding_box
+ dx = x*k-@x1
+ dy = @hPt-@y2-y*k
+ else
+ dx = x*k
+ dy = -y*k
+ end
+ tm = [1,0,0,1,dx,dy]
+ out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm',
+ tm[0], tm[1], tm[2], tm[3], tm[4], tm[5]))
+
+ if w > 0
+ scale_x = w/((@x2-@x1)/k)
+ if h > 0
+ scale_y = h/((@y2-@y1)/k)
+ else
+ scale_y = scale_x
+ h = (@y2-@y1)/k * scale_y
+ end
+ else
+ if h > 0
+ scale_y = $h/((@y2-@y1)/$k)
+ scale_x = scale_y
+ w = (@x2-@x1)/k * scale_x
+ else
+ w = (@x2-@x1)/k
+ h = (@y2-@y1)/k
+ end
+ end
+
+ if !scale_x.nil?
+ # Scale
+ tm = [scale_x,0,0,scale_y,0,@hPt*(1-scale_y)]
+ out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm',
+ tm[0], tm[1], tm[2], tm[3], tm[4], tm[5]))
+ end
+
+ data.split(/\r\n|[\r\n]/).each do |line|
+ next if line == '' || line[0,1] == '%'
+ len = line.length
+ # next if (len > 2 && line[len-2,len] != ' ')
+ cmd = line[len-2,len].strip
+ case cmd
+ when 'm', 'l', 'v', 'y', 'c', 'k', 'K', 'g', 'G', 's', 'S', 'J', 'j', 'w', 'M', 'd':
+ out(line)
+
+ when 'L':
+ line[len-1,len]='l'
+ out(line)
+
+ when 'C':
+ line[len-1,len]='c'
+ out(line)
+
+ when 'f', 'F':
+ out('f*')
+
+ when 'b', 'B':
+ out(cmd + '*')
+ end
+ end
+
+ # restore previous graphic state
+ out('Q')
+ Link(x,y,w,h,link) if link
+ end
+end
--- /dev/null
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+# 1.12 contributed by Ed Moss.
+#
+# The MIT License
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# This is direct port of japanese.php
+#
+# Japanese PDF support.
+#
+# Usage is as follows:
+#
+# require 'fpdf'
+# require 'chinese'
+# pdf = FPDF.new
+# pdf.extend(PDF_Japanese)
+#
+# This allows it to be combined with other extensions, such as the bookmark
+# module.
+
+module PDF_Japanese
+
+ SJIS_widths={' ' => 278, '!' => 299, '"' => 353, '#' => 614, '$' => 614, '%' => 721, '&' => 735, '\'' => 216,
+ '(' => 323, ')' => 323, '*' => 449, '+' => 529, ',' => 219, '-' => 306, '.' => 219, '/' => 453, '0' => 614, '1' => 614,
+ '2' => 614, '3' => 614, '4' => 614, '5' => 614, '6' => 614, '7' => 614, '8' => 614, '9' => 614, ':' => 219, ';' => 219,
+ '<' => 529, '=' => 529, '>' => 529, '?' => 486, '@' => 744, 'A' => 646, 'B' => 604, 'C' => 617, 'D' => 681, 'E' => 567,
+ 'F' => 537, 'G' => 647, 'H' => 738, 'I' => 320, 'J' => 433, 'K' => 637, 'L' => 566, 'M' => 904, 'N' => 710, 'O' => 716,
+ 'P' => 605, 'Q' => 716, 'R' => 623, 'S' => 517, 'T' => 601, 'U' => 690, 'V' => 668, 'W' => 990, 'X' => 681, 'Y' => 634,
+ 'Z' => 578, '[' => 316, '\\' => 614, ']' => 316, '^' => 529, '_' => 500, '`' => 387, 'a' => 509, 'b' => 566, 'c' => 478,
+ 'd' => 565, 'e' => 503, 'f' => 337, 'g' => 549, 'h' => 580, 'i' => 275, 'j' => 266, 'k' => 544, 'l' => 276, 'm' => 854,
+ 'n' => 579, 'o' => 550, 'p' => 578, 'q' => 566, 'r' => 410, 's' => 444, 't' => 340, 'u' => 575, 'v' => 512, 'w' => 760,
+ 'x' => 503, 'y' => 529, 'z' => 453, '{' => 326, '|' => 380, '}' => 326, '~' => 387}
+
+ def AddCIDFont(family,style,name,cw,cMap,registry)
+ fontkey=family.downcase+style.upcase
+ unless @fonts[fontkey].nil?
+ Error("CID font already added: family style")
+ end
+ i=@fonts.length+1
+ @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-120,'ut'=>40,'cw'=>cw,
+ 'CMap'=>cMap,'registry'=>registry}
+ end
+
+ def AddCIDFonts(family,name,cw,cMap,registry)
+ AddCIDFont(family,'',name,cw,cMap,registry)
+ AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
+ AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
+ AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
+ end
+
+ def AddSJISFont(family='SJIS')
+ #Add SJIS font with proportional Latin
+ name='KozMinPro-Regular-Acro'
+ cw=SJIS_widths
+ cMap='90msp-RKSJ-H'
+ registry={'ordering'=>'Japan1','supplement'=>2}
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def AddSJIShwFont(family='SJIS-hw')
+ #Add SJIS font with half-width Latin
+ name='KozMinPro-Regular-Acro'
+ 32.upto(126) do |i|
+ cw[i.chr]=500
+ end
+ cMap='90ms-RKSJ-H'
+ registry={'ordering'=>'Japan1','supplement'=>2}
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def GetStringWidth(s)
+ if(@CurrentFont['type']=='Type0')
+ return GetSJISStringWidth(s)
+ else
+ return super(s)
+ end
+ end
+
+ def GetSJISStringWidth(s)
+ #SJIS version of GetStringWidth()
+ l=0
+ cw=@CurrentFont['cw']
+ nb=s.length
+ i=0
+ while(i<nb)
+ o=s[i]
+ if(o<128)
+ #ASCII
+ l+=cw[o.chr]
+ i+=1
+ elsif(o>=161 and o<=223)
+ #Half-width katakana
+ l+=500
+ i+=1
+ else
+ #Full-width character
+ l+=1000
+ i+=2
+ end
+ end
+ return l*@FontSize/1000
+ end
+
+ def MultiCell(w,h,txt,border=0,align='L',fill=0)
+ if(@CurrentFont['type']=='Type0')
+ SJISMultiCell(w,h,txt,border,align,fill)
+ else
+ super(w,h,txt,border,align,fill)
+ end
+ end
+
+ def SJISMultiCell(w,h,txt,border=0,align='L',fill=0)
+ #Output text with automatic or explicit line breaks
+ cw=@CurrentFont['cw']
+ if(w==0)
+ w=@w-@rMargin-@x
+ end
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ if(nb>0 and s[nb-1]=="\n")
+ nb-=1
+ end
+ b=0
+ if(border)
+ if(border==1)
+ border='LTRB'
+ b='LRT'
+ b2='LR'
+ else
+ b2=''
+ if(border.to_s.index('L'))
+ b2+='L'
+ end
+ if(border.to_s.index('R'))
+ b2+='R'
+ end
+ b=border.to_s.index('T') ? b2+'T' : b2
+ end
+ end
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ o=c #o=ord(c)
+ if(o==10)
+ #Explicit line break
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ nl+=1
+ if(border and nl==2)
+ b=b2
+ end
+ next
+ end
+ if(o<128)
+ #ASCII
+ l+=cw[c.chr]
+ n=1
+ if(o==32)
+ sep=i
+ end
+ elsif(o>=161 and o<=223)
+ #Half-width katakana
+ l+=500
+ n=1
+ sep=i
+ else
+ #Full-width character
+ l+=1000
+ n=2
+ sep=i
+ end
+ if(l>wmax)
+ #Automatic line break
+ if(sep==-1 or i==j)
+ if(i==j)
+ i+=n
+ end
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ else
+ Cell(w,h,s[j,sep-j],b,2,align,fill)
+ i=(s[sep]==' ') ? sep+1 : sep
+ end
+ sep=-1
+ j=i
+ l=0
+ nl+=1
+ if(border and nl==2)
+ b=b2
+ end
+ else
+ i+=n
+ if(o>=128)
+ sep=i
+ end
+ end
+ end
+ #Last chunk
+ if(border and not border.to_s.index('B').nil?)
+ b+='B'
+ end
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ @x=@lMargin
+ end
+
+ def Write(h,txt,link='')
+ if(@CurrentFont['type']=='Type0')
+ SJISWrite(h,txt,link)
+ else
+ super(h,txt,link)
+ end
+ end
+
+ def SJISWrite(h,txt,link)
+ #SJIS version of Write()
+ cw=@CurrentFont['cw']
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ o=c
+ if(o==10)
+ #Explicit line break
+ Cell(w,h,s[j,i-j],0,2,'',0,link)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ if(nl==1)
+ #Go to left margin
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl+=1
+ next
+ end
+ if(o<128)
+ #ASCII
+ l+=cw[c.chr]
+ n=1
+ if(o==32)
+ sep=i
+ end
+ elsif(o>=161 and o<=223)
+ #Half-width katakana
+ l+=500
+ n=1
+ sep=i
+ else
+ #Full-width character
+ l+=1000
+ n=2
+ sep=i
+ end
+ if(l>wmax)
+ #Automatic line break
+ if(sep==-1 or i==j)
+ if(@x>@lMargin)
+ #Move to next line
+ @x=@lMargin
+ @y+=h
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ i+=n
+ nl+=1
+ next
+ end
+ if(i==j)
+ i+=n
+ end
+ Cell(w,h,s[j,i-j],0,2,'',0,link)
+ else
+ Cell(w,h,s[j,sep-j],0,2,'',0,link)
+ i=(s[sep]==' ') ? sep+1 : sep
+ end
+ sep=-1
+ j=i
+ l=0
+ if(nl==1)
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl+=1
+ else
+ i+=n
+ if(o>=128)
+ sep=i
+ end
+ end
+ end
+ #Last chunk
+ if(i!=j)
+ Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)
+ end
+ end
+
+private
+
+ def putfonts()
+ nf=@n
+ @diffs.each do |diff|
+ #Encodings
+ newobj()
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
+ out('endobj')
+ end
+ # mqr=get_magic_quotes_runtime()
+ # set_magic_quotes_runtime(0)
+ @FontFiles.each_pair do |file, info|
+ #Font file embedding
+ newobj()
+ @FontFiles[file]['n']=@n
+ if(defined('FPDF_FONTPATH'))
+ file=FPDF_FONTPATH+file
+ end
+ size=filesize(file)
+ if(!size)
+ Error('Font file not found')
+ end
+ out('<</Length '+size)
+ if(file[-2]=='.z')
+ out('/Filter /FlateDecode')
+ end
+ out('/Length1 '+info['length1'])
+ unless info['length2'].nil?
+ out('/Length2 '+info['length2']+' /Length3 0')
+ end
+ out('>>')
+ f=fopen(file,'rb')
+ putstream(fread(f,size))
+ fclose(f)
+ out('endobj')
+ end
+ # set_magic_quotes_runtime(mqr)
+ @fonts.each_pair do |k, font|
+ #Font objects
+ newobj()
+ @fonts[k]['n']=@n
+ out('<</Type /Font')
+ if(font['type']=='Type0')
+ putType0(font)
+ else
+ name=font['name']
+ out('/BaseFont /'+name)
+ if(font['type']=='core')
+ #Standard font
+ out('/Subtype /Type1')
+ if(name!='Symbol' and name!='ZapfDingbats')
+ out('/Encoding /WinAnsiEncoding')
+ end
+ else
+ #Additional font
+ out('/Subtype /'+font['type'])
+ out('/FirstChar 32')
+ out('/LastChar 255')
+ out('/Widths '+(@n+1)+' 0 R')
+ out('/FontDescriptor '+(@n+2)+' 0 R')
+ if(font['enc'])
+ if !font['diff'].nil?
+ out('/Encoding '+(nf+font['diff'])+' 0 R')
+ else
+ out('/Encoding /WinAnsiEncoding')
+ end
+ end
+ end
+ out('>>')
+ out('endobj')
+ if(font['type']!='core')
+ #Widths
+ newobj()
+ cw=font['cw']
+ s='['
+ 32.upto(255) do |i|
+ s+=cw[i.chr]+' '
+ end
+ out(s+']')
+ out('endobj')
+ #Descriptor
+ newobj()
+ s='<</Type /FontDescriptor /FontName /'+name
+ font['desc'].each_pair do |k, v|
+ s+=' /'+k+' '+v
+ end
+ file=font['file']
+ if(file)
+ s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'
+ end
+ out(s+'>>')
+ out('endobj')
+ end
+ end
+ end
+ end
+
+ def putType0(font)
+ #Type0
+ out('/Subtype /Type0')
+ out('/BaseFont /'+font['name']+'-'+font['CMap'])
+ out('/Encoding /'+font['CMap'])
+ out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
+ out('>>')
+ out('endobj')
+ #CIDFont
+ newobj()
+ out('<</Type /Font')
+ out('/Subtype /CIDFontType0')
+ out('/BaseFont /'+font['name'])
+ out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
+ 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('<</Type /FontDescriptor')
+ out('/FontName /'+font['name'])
+ out('/Flags 6')
+ out('/FontBBox [0 -200 1000 900]')
+ out('/ItalicAngle 0')
+ out('/Ascent 800')
+ out('/Descent -200')
+ out('/CapHeight 800')
+ out('/StemV 60')
+ out('>>')
+ out('endobj')
+ end
+end
--- /dev/null
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>\r
+# 1.12 contributed by Ed Moss.\r
+#\r
+# The MIT License\r
+#\r
+# Permission is hereby granted, free of charge, to any person obtaining a copy\r
+# of this software and associated documentation files (the "Software"), to deal\r
+# in the Software without restriction, including without limitation the rights\r
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+# copies of the Software, and to permit persons to whom the Software is\r
+# furnished to do so, subject to the following conditions:\r
+#\r
+# The above copyright notice and this permission notice shall be included in\r
+# all copies or substantial portions of the Software.\r
+#\r
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+# THE SOFTWARE.\r
+#\r
+# This is direct port of korean.php\r
+#\r
+# Korean PDF support.\r
+#\r
+# Usage is as follows:\r
+#\r
+# require 'fpdf'\r
+# require 'chinese'\r
+# pdf = FPDF.new\r
+# pdf.extend(PDF_Korean)\r
+#\r
+# This allows it to be combined with other extensions, such as the bookmark\r
+# module.\r
+\r
+module PDF_Korean\r
+\r
+UHC_widths={' ' => 333, '!' => 416, '"' => 416, '#' => 833, '$' => 625, '%' => 916, '&' => 833, '\'' => 250, \r
+ '(' => 500, ')' => 500, '*' => 500, '+' => 833, ',' => 291, '-' => 833, '.' => 291, '/' => 375, '0' => 625, '1' => 625, \r
+ '2' => 625, '3' => 625, '4' => 625, '5' => 625, '6' => 625, '7' => 625, '8' => 625, '9' => 625, ':' => 333, ';' => 333, \r
+ '<' => 833, '=' => 833, '>' => 916, '?' => 500, '@' => 1000, 'A' => 791, 'B' => 708, 'C' => 708, 'D' => 750, 'E' => 708, \r
+ 'F' => 666, 'G' => 750, 'H' => 791, 'I' => 375, 'J' => 500, 'K' => 791, 'L' => 666, 'M' => 916, 'N' => 791, 'O' => 750, \r
+ 'P' => 666, 'Q' => 750, 'R' => 708, 'S' => 666, 'T' => 791, 'U' => 791, 'V' => 750, 'W' => 1000, 'X' => 708, 'Y' => 708, \r
+ 'Z' => 666, '[' => 500, '\\' => 375, ']' => 500, '^' => 500, '_' => 500, '`' => 333, 'a' => 541, 'b' => 583, 'c' => 541, \r
+ 'd' => 583, 'e' => 583, 'f' => 375, 'g' => 583, 'h' => 583, 'i' => 291, 'j' => 333, 'k' => 583, 'l' => 291, 'm' => 875, \r
+ 'n' => 583, 'o' => 583, 'p' => 583, 'q' => 583, 'r' => 458, 's' => 541, 't' => 375, 'u' => 583, 'v' => 583, 'w' => 833, \r
+ 'x' => 625, 'y' => 625, 'z' => 500, '{' => 583, '|' => 583, '}' => 583, '~' => 750}\r
+\r
+ def AddCIDFont(family,style,name,cw,cMap,registry)\r
+ fontkey=family.downcase+style.upcase\r
+ unless @fonts[fontkey].nil?\r
+ Error("Font already added: family style")\r
+ end\r
+ i=@fonts.length+1\r
+ name=name.gsub(' ','')\r
+ @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw,\r
+ 'CMap'=>cMap,'registry'=>registry}\r
+ end\r
+\r
+ def AddCIDFonts(family,name,cw,cMap,registry)\r
+ AddCIDFont(family,'',name,cw,cMap,registry)\r
+ AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)\r
+ AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)\r
+ AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)\r
+ end\r
+\r
+ def AddUHCFont(family='UHC',name='HYSMyeongJoStd-Medium-Acro')\r
+ #Add UHC font with proportional Latin\r
+ cw=UHC_widths\r
+ cMap='KSCms-UHC-H'\r
+ registry={'ordering'=>'Korea1','supplement'=>1}\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def AddUHChwFont(family='UHC-hw',name='HYSMyeongJoStd-Medium-Acro')\r
+ #Add UHC font with half-witdh Latin\r
+ 32.upto(126) do |i|\r
+ cw[i.chr]=500\r
+ end\r
+ cMap='KSCms-UHC-HW-H'\r
+ registry={'ordering'=>'Korea1','supplement'=>1}\r
+ AddCIDFonts(family,name,cw,cMap,registry)\r
+ end\r
+\r
+ def GetStringWidth(s)\r
+ if(@CurrentFont['type']=='Type0')\r
+ return GetMBStringWidth(s)\r
+ else\r
+ return super(s)\r
+ end\r
+ end\r
+\r
+ def GetMBStringWidth(s)\r
+ #Multi-byte version of GetStringWidth()\r
+ l=0\r
+ cw=@CurrentFont['cw']\r
+ nb=s.length\r
+ i=0\r
+ while(i<nb)\r
+ c=s[i]\r
+ if(c<128)\r
+ l+=cw[c.chr]\r
+ i+=1\r
+ else\r
+ l+=1000\r
+ i+=2\r
+ end\r
+ end\r
+ return l*@FontSize/1000\r
+ end\r
+\r
+ def MultiCell(w,h,txt,border=0,align='L',fill=0)\r
+ if(@CurrentFont['type']=='Type0')\r
+ MBMultiCell(w,h,txt,border,align,fill)\r
+ else\r
+ super(w,h,txt,border,align,fill)\r
+ end\r
+ end\r
+\r
+ def MBMultiCell(w,h,txt,border=0,align='L',fill=0)\r
+ #Multi-byte version of MultiCell()\r
+ cw=@CurrentFont['cw']\r
+ if(w==0)\r
+ w=@w-@rMargin-@x\r
+ end\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ s=txt.gsub("\r",'')\r
+ nb=s.length\r
+ if(nb>0 and s[nb-1]=="\n")\r
+ nb-=1\r
+ end\r
+ b=0\r
+ if(border)\r
+ if(border==1)\r
+ border='LTRB'\r
+ b='LRT'\r
+ b2='LR'\r
+ else\r
+ b2=''\r
+ if(border.index('L').nil?)\r
+ b2+='L'\r
+ end\r
+ if(border.index('R').nil?)\r
+ b2+='R'\r
+ end\r
+ b=border.index('T').nil? ? b2+'T' : b2\r
+ end\r
+ end\r
+ sep=-1\r
+ i=0\r
+ j=0\r
+ l=0\r
+ nl=1\r
+ while(i<nb)\r
+ #Get next character\r
+ c=s[i]\r
+ #Check if ASCII or MB\r
+ ascii=(c<128)\r
+ if(c=="\n")\r
+ #Explicit line break\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ i+=1\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ nl+=1\r
+ if(border and nl==2)\r
+ b=b2\r
+ end\r
+ next\r
+ end\r
+ if(!ascii)\r
+ sep=i\r
+ ls=l\r
+ elsif(c==' ')\r
+ sep=i\r
+ ls=l\r
+ end\r
+ l+=ascii ? cw[c.chr] : 1000\r
+ if(l>wmax)\r
+ #Automatic line break\r
+ if(sep==-1 or i==j)\r
+ if(i==j)\r
+ i+=ascii ? 1 : 2\r
+ end\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ else\r
+ Cell(w,h,s[j,sep-j],b,2,align,fill)\r
+ i=(s[sep]==' ') ? sep+1 : sep\r
+ end\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ nl+=1\r
+ if(border and nl==2)\r
+ b=b2\r
+ end\r
+ else\r
+ i+=ascii ? 1 : 2\r
+ end\r
+ end\r
+ #Last chunk\r
+ if(border and not border.index('B').nil?)\r
+ b+='B'\r
+ end\r
+ Cell(w,h,s[j,i-j],b,2,align,fill)\r
+ @x=@lMargin\r
+ end\r
+\r
+ def Write(h,txt,link='')\r
+ if(@CurrentFont['type']=='Type0')\r
+ MBWrite(h,txt,link)\r
+ else\r
+ super(h,txt,link)\r
+ end\r
+ end\r
+\r
+ def MBWrite(h,txt,link)\r
+ #Multi-byte version of Write()\r
+ cw=@CurrentFont['cw']\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ s=txt.gsub("\r",'')\r
+ nb=s.length\r
+ sep=-1\r
+ i=0\r
+ j=0\r
+ l=0\r
+ nl=1\r
+ while(i<nb)\r
+ #Get next character\r
+ c=s[i]\r
+ #Check if ASCII or MB\r
+ ascii=(c<128)\r
+ if(c=="\n")\r
+ #Explicit line break\r
+ Cell(w,h,s[j,i-j],0,2,'',0,link)\r
+ i+=1\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ if(nl==1)\r
+ @x=@lMargin\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ end\r
+ nl+=1\r
+ next\r
+ end\r
+ if(!ascii or c==' ')\r
+ sep=i\r
+ end\r
+ l+=ascii ? cw[c.chr] : 1000\r
+ if(l>wmax)\r
+ #Automatic line break\r
+ if(sep==-1 or i==j)\r
+ if(@x>@lMargin)\r
+ #Move to next line\r
+ @x=@lMargin\r
+ @y+=h\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ i+=1\r
+ nl+=1\r
+ next\r
+ end\r
+ if(i==j)\r
+ i+=ascii ? 1 : 2\r
+ end\r
+ Cell(w,h,s[j,i-j],0,2,'',0,link)\r
+ else\r
+ Cell(w,h,s[j,sep-j],0,2,'',0,link)\r
+ i=(s[sep]==' ') ? sep+1 : sep\r
+ end\r
+ sep=-1\r
+ j=i\r
+ l=0\r
+ if(nl==1)\r
+ @x=@lMargin\r
+ w=@w-@rMargin-@x\r
+ wmax=(w-2*@cMargin)*1000/@FontSize\r
+ end\r
+ nl+=1\r
+ else\r
+ i+=ascii ? 1 : 2\r
+ end\r
+ end\r
+ #Last chunk\r
+ if(i!=j)\r
+ Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)\r
+ end\r
+ end\r
+\r
+private\r
+\r
+ def putfonts()\r
+ nf=@n\r
+ @diffs.each do |diff|\r
+ #Encodings\r
+ newobj()\r
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')\r
+ out('endobj')\r
+ end\r
+ # mqr=get_magic_quotes_runtime()\r
+ # set_magic_quotes_runtime(0)\r
+ @FontFiles.each_pair do |file, info|\r
+ #Font file embedding\r
+ newobj()\r
+ @FontFiles[file]['n']=@n\r
+ if(defined('FPDF_FONTPATH'))\r
+ file=FPDF_FONTPATH+file\r
+ end\r
+ size=filesize(file)\r
+ if(!size)\r
+ Error('Font file not found')\r
+ end\r
+ out('<</Length '+size)\r
+ if(file[-2]=='.z')\r
+ out('/Filter /FlateDecode')\r
+ end\r
+ out('/Length1 '+info['length1'])\r
+ if(not info['length2'].nil?)\r
+ out('/Length2 '+info['length2']+' /Length3 0')\r
+ end\r
+ out('>>')\r
+ f=fopen(file,'rb')\r
+ putstream(fread(f,size))\r
+ fclose(f)\r
+ out('endobj')\r
+ end\r
+ # set_magic_quotes_runtime(mqr)\r
+ @fonts.each_pair do |k, font|\r
+ #Font objects\r
+ newobj()\r
+ @fonts[k]['n']=@n\r
+ out('<</Type /Font')\r
+ if(font['type']=='Type0')\r
+ putType0(font)\r
+ else\r
+ name=font['name']\r
+ out('/BaseFont /'+name)\r
+ if(font['type']=='core')\r
+ #Standard font\r
+ out('/Subtype /Type1')\r
+ if(name!='Symbol' and name!='ZapfDingbats')\r
+ out('/Encoding /WinAnsiEncoding')\r
+ end\r
+ else\r
+ #Additional font\r
+ out('/Subtype /'+font['type'])\r
+ out('/FirstChar 32')\r
+ out('/LastChar 255')\r
+ out('/Widths '+(@n+1)+' 0 R')\r
+ out('/FontDescriptor '+(@n+2)+' 0 R')\r
+ if(font['enc'])\r
+ if(not font['diff'].nil?)\r
+ out('/Encoding '+(nf+font['diff'])+' 0 R')\r
+ else\r
+ out('/Encoding /WinAnsiEncoding')\r
+ end\r
+ end\r
+ end\r
+ out('>>')\r
+ out('endobj')\r
+ if(font['type']!='core')\r
+ #Widths\r
+ newobj()\r
+ cw=font['cw']\r
+ s='['\r
+ 32.upto(255) do |i|\r
+ s+=cw[i.chr]+' '\r
+ end\r
+ out(s+']')\r
+ out('endobj')\r
+ #Descriptor\r
+ newobj()\r
+ s='<</Type /FontDescriptor /FontName /'+name\r
+ font['desc'].each_pair do |k, v| \r
+ s+=' /'+k+' '+v\r
+ end\r
+ file=font['file']\r
+ if(file)\r
+ s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'\r
+ end\r
+ out(s+'>>')\r
+ out('endobj')\r
+ end\r
+ end\r
+ end\r
+ end\r
+ \r
+ def putType0(font)\r
+ #Type0\r
+ out('/Subtype /Type0')\r
+ out('/BaseFont /'+font['name']+'-'+font['CMap'])\r
+ out('/Encoding /'+font['CMap'])\r
+ out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')\r
+ out('>>')\r
+ out('endobj')\r
+ #CIDFont\r
+ newobj()\r
+ out('<</Type /Font')\r
+ out('/Subtype /CIDFontType0')\r
+ out('/BaseFont /'+font['name'])\r
+ out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')\r
+ out('/FontDescriptor '+(@n+1).to_s+' 0 R')\r
+ if(font['CMap']=='KSCms-UHC-HW-H')\r
+ w='8094 8190 500'\r
+ else\r
+ w='1 ['\r
+ font['cw'].keys.sort.each {|key|\r
+ w+=font['cw'][key].to_s + " "\r
+ # ActionController::Base::logger.debug key.to_s\r
+ # ActionController::Base::logger.debug font['cw'][key].to_s\r
+ }\r
+ w +=']'\r
+ end\r
+ out('/W ['+w+']>>')\r
+ out('endobj')\r
+ #Font descriptor\r
+ newobj()\r
+ out('<</Type /FontDescriptor')\r
+ out('/FontName /'+font['name'])\r
+ out('/Flags 6')\r
+ out('/FontBBox [0 -200 1000 900]')\r
+ out('/ItalicAngle 0')\r
+ out('/Ascent 800')\r
+ out('/Descent -200')\r
+ out('/CapHeight 800')\r
+ out('/StemV 50')\r
+ out('>>')\r
+ out('endobj')\r
+ end\r
+end\r
--- /dev/null
+#!/usr/bin/env ruby
+#
+# Utility to generate font definition files
+# Version: 1.1
+# Date: 2006-07-19
+#
+# Changelog:
+# Version 1.1 - Brian Ollenberger
+# - Fixed a very small bug in MakeFont for generating FontDef.diff.
+
+Charencodings = {
+# Central Europe
+ 'cp1250' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', '.notdef',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ '.notdef', 'perthousand', 'Scaron', 'guilsinglleft',
+ 'Sacute', 'Tcaron', 'Zcaron', 'Zacute',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ '.notdef', 'trademark', 'scaron', 'guilsinglright',
+ 'sacute', 'tcaron', 'zcaron', 'zacute',
+ 'space', 'caron', 'breve', 'Lslash',
+ 'currency', 'Aogonek', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'Scedilla', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'Zdotaccent',
+ 'degree', 'plusminus', 'ogonek', 'lslash',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'aogonek', 'scedilla', 'guillemotright',
+ 'Lcaron', 'hungarumlaut', 'lcaron', 'zdotaccent',
+ 'Racute', 'Aacute', 'Acircumflex', 'Abreve',
+ 'Adieresis', 'Lacute', 'Cacute', 'Ccedilla',
+ 'Ccaron', 'Eacute', 'Eogonek', 'Edieresis',
+ 'Ecaron', 'Iacute', 'Icircumflex', 'Dcaron',
+ 'Dcroat', 'Nacute', 'Ncaron', 'Oacute',
+ 'Ocircumflex', 'Ohungarumlaut', 'Odieresis', 'multiply',
+ 'Rcaron', 'Uring', 'Uacute', 'Uhungarumlaut',
+ 'Udieresis', 'Yacute', 'Tcommaaccent', 'germandbls',
+ 'racute', 'aacute', 'acircumflex', 'abreve',
+ 'adieresis', 'lacute', 'cacute', 'ccedilla',
+ 'ccaron', 'eacute', 'eogonek', 'edieresis',
+ 'ecaron', 'iacute', 'icircumflex', 'dcaron',
+ 'dcroat', 'nacute', 'ncaron', 'oacute',
+ 'ocircumflex', 'ohungarumlaut', 'odieresis', 'divide',
+ 'rcaron', 'uring', 'uacute', 'uhungarumlaut',
+ 'udieresis', 'yacute', 'tcommaaccent', 'dotaccent'
+ ],
+# Cyrillic
+ 'cp1251' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'afii10051', 'afii10052', 'quotesinglbase', 'afii10100',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ 'Euro', 'perthousand', 'afii10058', 'guilsinglleft',
+ 'afii10059', 'afii10061', 'afii10060', 'afii10145',
+ 'afii10099', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ '.notdef', 'trademark', 'afii10106', 'guilsinglright',
+ 'afii10107', 'afii10109', 'afii10108', 'afii10193',
+ 'space', 'afii10062', 'afii10110', 'afii10057',
+ 'currency', 'afii10050', 'brokenbar', 'section',
+ 'afii10023', 'copyright', 'afii10053', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'afii10056',
+ 'degree', 'plusminus', 'afii10055', 'afii10103',
+ 'afii10098', 'mu', 'paragraph', 'periodcentered',
+ 'afii10071', 'afii61352', 'afii10101', 'guillemotright',
+ 'afii10105', 'afii10054', 'afii10102', 'afii10104',
+ 'afii10017', 'afii10018', 'afii10019', 'afii10020',
+ 'afii10021', 'afii10022', 'afii10024', 'afii10025',
+ 'afii10026', 'afii10027', 'afii10028', 'afii10029',
+ 'afii10030', 'afii10031', 'afii10032', 'afii10033',
+ 'afii10034', 'afii10035', 'afii10036', 'afii10037',
+ 'afii10038', 'afii10039', 'afii10040', 'afii10041',
+ 'afii10042', 'afii10043', 'afii10044', 'afii10045',
+ 'afii10046', 'afii10047', 'afii10048', 'afii10049',
+ 'afii10065', 'afii10066', 'afii10067', 'afii10068',
+ 'afii10069', 'afii10070', 'afii10072', 'afii10073',
+ 'afii10074', 'afii10075', 'afii10076', 'afii10077',
+ 'afii10078', 'afii10079', 'afii10080', 'afii10081',
+ 'afii10082', 'afii10083', 'afii10084', 'afii10085',
+ 'afii10086', 'afii10087', 'afii10088', 'afii10089',
+ 'afii10090', 'afii10091', 'afii10092', 'afii10093',
+ 'afii10094', 'afii10095', 'afii10096', 'afii10097'
+ ],
+# Western Europe
+ 'cp1252' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', 'florin',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ 'circumflex', 'perthousand', 'Scaron', 'guilsinglleft',
+ 'OE', '.notdef', 'Zcaron', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ 'tilde', 'trademark', 'scaron', 'guilsinglright',
+ 'oe', '.notdef', 'zcaron', 'Ydieresis',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Eth', 'Ntilde', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Yacute', 'Thorn', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'eth', 'ntilde', 'ograve', 'oacute',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'yacute', 'thorn', 'ydieresis'
+ ],
+# Greek
+ 'cp1253' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', 'florin',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ '.notdef', 'perthousand', '.notdef', 'guilsinglleft',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ '.notdef', 'trademark', '.notdef', 'guilsinglright',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'dieresistonos', 'Alphatonos', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', '.notdef', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'afii00208',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'tonos', 'mu', 'paragraph', 'periodcentered',
+ 'Epsilontonos', 'Etatonos', 'Iotatonos', 'guillemotright',
+ 'Omicrontonos', 'onehalf', 'Upsilontonos', 'Omegatonos',
+ 'iotadieresistonos','Alpha', 'Beta', 'Gamma',
+ 'Delta', 'Epsilon', 'Zeta', 'Eta',
+ 'Theta', 'Iota', 'Kappa', 'Lambda',
+ 'Mu', 'Nu', 'Xi', 'Omicron',
+ 'Pi', 'Rho', '.notdef', 'Sigma',
+ 'Tau', 'Upsilon', 'Phi', 'Chi',
+ 'Psi', 'Omega', 'Iotadieresis', 'Upsilondieresis',
+ 'alphatonos', 'epsilontonos', 'etatonos', 'iotatonos',
+ 'upsilondieresistonos','alpha', 'beta', 'gamma',
+ 'delta', 'epsilon', 'zeta', 'eta',
+ 'theta', 'iota', 'kappa', 'lambda',
+ 'mu', 'nu', 'xi', 'omicron',
+ 'pi', 'rho', 'sigma1', 'sigma',
+ 'tau', 'upsilon', 'phi', 'chi',
+ 'psi', 'omega', 'iotadieresis', 'upsilondieresis',
+ 'omicrontonos', 'upsilontonos', 'omegatonos', '.notdef'
+ ],
+# Turkish
+ 'cp1254' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', 'florin',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ 'circumflex', 'perthousand', 'Scaron', 'guilsinglleft',
+ 'OE', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ 'tilde', 'trademark', 'scaron', 'guilsinglright',
+ 'oe', '.notdef', '.notdef', 'Ydieresis',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Gbreve', 'Ntilde', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Idotaccent', 'Scedilla', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'gbreve', 'ntilde', 'ograve', 'oacute',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'dotlessi', 'scedilla', 'ydieresis'
+ ],
+# Hebrew
+ 'cp1255' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', 'florin',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ 'circumflex', 'perthousand', '.notdef', 'guilsinglleft',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ 'tilde', 'trademark', '.notdef', 'guilsinglright',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'afii57636', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'multiply', 'guillemotleft',
+ 'logicalnot', 'sfthyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'middot',
+ 'cedilla', 'onesuperior', 'divide', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'afii57799', 'afii57801', 'afii57800', 'afii57802',
+ 'afii57793', 'afii57794', 'afii57795', 'afii57798',
+ 'afii57797', 'afii57806', '.notdef', 'afii57796',
+ 'afii57807', 'afii57839', 'afii57645', 'afii57841',
+ 'afii57842', 'afii57804', 'afii57803', 'afii57658',
+ 'afii57716', 'afii57717', 'afii57718', 'gereshhebrew',
+ 'gershayimhebrew','.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'afii57664', 'afii57665', 'afii57666', 'afii57667',
+ 'afii57668', 'afii57669', 'afii57670', 'afii57671',
+ 'afii57672', 'afii57673', 'afii57674', 'afii57675',
+ 'afii57676', 'afii57677', 'afii57678', 'afii57679',
+ 'afii57680', 'afii57681', 'afii57682', 'afii57683',
+ 'afii57684', 'afii57685', 'afii57686', 'afii57687',
+ 'afii57688', 'afii57689', 'afii57690', '.notdef',
+ '.notdef', 'afii299', 'afii300', '.notdef'
+ ],
+# Baltic
+ 'cp1257' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', '.notdef',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ '.notdef', 'perthousand', '.notdef', 'guilsinglleft',
+ '.notdef', 'dieresis', 'caron', 'cedilla',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ '.notdef', 'trademark', '.notdef', 'guilsinglright',
+ '.notdef', 'macron', 'ogonek', '.notdef',
+ 'space', '.notdef', 'cent', 'sterling',
+ 'currency', '.notdef', 'brokenbar', 'section',
+ 'Oslash', 'copyright', 'Rcommaaccent', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'AE',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'oslash', 'onesuperior', 'rcommaaccent', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'ae',
+ 'Aogonek', 'Iogonek', 'Amacron', 'Cacute',
+ 'Adieresis', 'Aring', 'Eogonek', 'Emacron',
+ 'Ccaron', 'Eacute', 'Zacute', 'Edotaccent',
+ 'Gcommaaccent', 'Kcommaaccent', 'Imacron', 'Lcommaaccent',
+ 'Scaron', 'Nacute', 'Ncommaaccent', 'Oacute',
+ 'Omacron', 'Otilde', 'Odieresis', 'multiply',
+ 'Uogonek', 'Lslash', 'Sacute', 'Umacron',
+ 'Udieresis', 'Zdotaccent', 'Zcaron', 'germandbls',
+ 'aogonek', 'iogonek', 'amacron', 'cacute',
+ 'adieresis', 'aring', 'eogonek', 'emacron',
+ 'ccaron', 'eacute', 'zacute', 'edotaccent',
+ 'gcommaaccent', 'kcommaaccent', 'imacron', 'lcommaaccent',
+ 'scaron', 'nacute', 'ncommaaccent', 'oacute',
+ 'omacron', 'otilde', 'odieresis', 'divide',
+ 'uogonek', 'lslash', 'sacute', 'umacron',
+ 'udieresis', 'zdotaccent', 'zcaron', 'dotaccent'
+ ],
+# Vietnamese
+ 'cp1258' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', 'quotesinglbase', 'florin',
+ 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl',
+ 'circumflex', 'perthousand', '.notdef', 'guilsinglleft',
+ 'OE', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ 'tilde', 'trademark', '.notdef', 'guilsinglright',
+ 'oe', '.notdef', '.notdef', 'Ydieresis',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Abreve',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'gravecomb', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Dcroat', 'Ntilde', 'hookabovecomb', 'Oacute',
+ 'Ocircumflex', 'Ohorn', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Uhorn', 'tildecomb', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'abreve',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'acutecomb', 'iacute', 'icircumflex', 'idieresis',
+ 'dcroat', 'ntilde', 'dotbelowcomb', 'oacute',
+ 'ocircumflex', 'ohorn', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'uhorn', 'dong', 'ydieresis'
+ ],
+# Thai
+ 'cp874' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'Euro', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'ellipsis', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', 'quoteleft', 'quoteright', 'quotedblleft',
+ 'quotedblright', 'bullet', 'endash', 'emdash',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'kokaithai', 'khokhaithai', 'khokhuatthai',
+ 'khokhwaithai', 'khokhonthai', 'khorakhangthai', 'ngonguthai',
+ 'chochanthai', 'chochingthai', 'chochangthai', 'sosothai',
+ 'chochoethai', 'yoyingthai', 'dochadathai', 'topatakthai',
+ 'thothanthai', 'thonangmonthothai', 'thophuthaothai', 'nonenthai',
+ 'dodekthai', 'totaothai', 'thothungthai', 'thothahanthai',
+ 'thothongthai', 'nonuthai', 'bobaimaithai', 'poplathai',
+ 'phophungthai', 'fofathai', 'phophanthai', 'fofanthai',
+ 'phosamphaothai', 'momathai', 'yoyakthai', 'roruathai',
+ 'ruthai', 'lolingthai', 'luthai', 'wowaenthai',
+ 'sosalathai', 'sorusithai', 'sosuathai', 'hohipthai',
+ 'lochulathai', 'oangthai', 'honokhukthai', 'paiyannoithai',
+ 'saraathai', 'maihanakatthai', 'saraaathai', 'saraamthai',
+ 'saraithai', 'saraiithai', 'sarauethai', 'saraueethai',
+ 'sarauthai', 'sarauuthai', 'phinthuthai', '.notdef',
+ '.notdef', '.notdef', '.notdef', 'bahtthai',
+ 'saraethai', 'saraaethai', 'saraothai', 'saraaimaimuanthai',
+ 'saraaimaimalaithai', 'lakkhangyaothai', 'maiyamokthai', 'maitaikhuthai',
+ 'maiekthai', 'maithothai', 'maitrithai', 'maichattawathai',
+ 'thanthakhatthai', 'nikhahitthai', 'yamakkanthai', 'fongmanthai',
+ 'zerothai', 'onethai', 'twothai', 'threethai',
+ 'fourthai', 'fivethai', 'sixthai', 'seventhai',
+ 'eightthai', 'ninethai', 'angkhankhuthai', 'khomutthai',
+ '.notdef', '.notdef', '.notdef', '.notdef'
+ ],
+# Western Europe
+ 'ISO-8859-1' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Eth', 'Ntilde', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Yacute', 'Thorn', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'eth', 'ntilde', 'ograve', 'oacute',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'yacute', 'thorn', 'ydieresis'
+ ],
+# Central Europe
+ 'ISO-8859-2' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'Aogonek', 'breve', 'Lslash',
+ 'currency', 'Lcaron', 'Sacute', 'section',
+ 'dieresis', 'Scaron', 'Scedilla', 'Tcaron',
+ 'Zacute', 'hyphen', 'Zcaron', 'Zdotaccent',
+ 'degree', 'aogonek', 'ogonek', 'lslash',
+ 'acute', 'lcaron', 'sacute', 'caron',
+ 'cedilla', 'scaron', 'scedilla', 'tcaron',
+ 'zacute', 'hungarumlaut', 'zcaron', 'zdotaccent',
+ 'Racute', 'Aacute', 'Acircumflex', 'Abreve',
+ 'Adieresis', 'Lacute', 'Cacute', 'Ccedilla',
+ 'Ccaron', 'Eacute', 'Eogonek', 'Edieresis',
+ 'Ecaron', 'Iacute', 'Icircumflex', 'Dcaron',
+ 'Dcroat', 'Nacute', 'Ncaron', 'Oacute',
+ 'Ocircumflex', 'Ohungarumlaut', 'Odieresis', 'multiply',
+ 'Rcaron', 'Uring', 'Uacute', 'Uhungarumlaut',
+ 'Udieresis', 'Yacute', 'Tcommaaccent', 'germandbls',
+ 'racute', 'aacute', 'acircumflex', 'abreve',
+ 'adieresis', 'lacute', 'cacute', 'ccedilla',
+ 'ccaron', 'eacute', 'eogonek', 'edieresis',
+ 'ecaron', 'iacute', 'icircumflex', 'dcaron',
+ 'dcroat', 'nacute', 'ncaron', 'oacute',
+ 'ocircumflex', 'ohungarumlaut', 'odieresis', 'divide',
+ 'rcaron', 'uring', 'uacute', 'uhungarumlaut',
+ 'udieresis', 'yacute', 'tcommaaccent', 'dotaccent'
+ ],
+# Baltic
+ 'ISO-8859-4' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'Aogonek', 'kgreenlandic', 'Rcommaaccent',
+ 'currency', 'Itilde', 'Lcommaaccent', 'section',
+ 'dieresis', 'Scaron', 'Emacron', 'Gcommaaccent',
+ 'Tbar', 'hyphen', 'Zcaron', 'macron',
+ 'degree', 'aogonek', 'ogonek', 'rcommaaccent',
+ 'acute', 'itilde', 'lcommaaccent', 'caron',
+ 'cedilla', 'scaron', 'emacron', 'gcommaaccent',
+ 'tbar', 'Eng', 'zcaron', 'eng',
+ 'Amacron', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Iogonek',
+ 'Ccaron', 'Eacute', 'Eogonek', 'Edieresis',
+ 'Edotaccent', 'Iacute', 'Icircumflex', 'Imacron',
+ 'Dcroat', 'Ncommaaccent', 'Omacron', 'Kcommaaccent',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Uogonek', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Utilde', 'Umacron', 'germandbls',
+ 'amacron', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'iogonek',
+ 'ccaron', 'eacute', 'eogonek', 'edieresis',
+ 'edotaccent', 'iacute', 'icircumflex', 'imacron',
+ 'dcroat', 'ncommaaccent', 'omacron', 'kcommaaccent',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'uogonek', 'uacute', 'ucircumflex',
+ 'udieresis', 'utilde', 'umacron', 'dotaccent'
+ ],
+# Cyrillic
+ 'ISO-8859-5' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'afii10023', 'afii10051', 'afii10052',
+ 'afii10053', 'afii10054', 'afii10055', 'afii10056',
+ 'afii10057', 'afii10058', 'afii10059', 'afii10060',
+ 'afii10061', 'hyphen', 'afii10062', 'afii10145',
+ 'afii10017', 'afii10018', 'afii10019', 'afii10020',
+ 'afii10021', 'afii10022', 'afii10024', 'afii10025',
+ 'afii10026', 'afii10027', 'afii10028', 'afii10029',
+ 'afii10030', 'afii10031', 'afii10032', 'afii10033',
+ 'afii10034', 'afii10035', 'afii10036', 'afii10037',
+ 'afii10038', 'afii10039', 'afii10040', 'afii10041',
+ 'afii10042', 'afii10043', 'afii10044', 'afii10045',
+ 'afii10046', 'afii10047', 'afii10048', 'afii10049',
+ 'afii10065', 'afii10066', 'afii10067', 'afii10068',
+ 'afii10069', 'afii10070', 'afii10072', 'afii10073',
+ 'afii10074', 'afii10075', 'afii10076', 'afii10077',
+ 'afii10078', 'afii10079', 'afii10080', 'afii10081',
+ 'afii10082', 'afii10083', 'afii10084', 'afii10085',
+ 'afii10086', 'afii10087', 'afii10088', 'afii10089',
+ 'afii10090', 'afii10091', 'afii10092', 'afii10093',
+ 'afii10094', 'afii10095', 'afii10096', 'afii10097',
+ 'afii61352', 'afii10071', 'afii10099', 'afii10100',
+ 'afii10101', 'afii10102', 'afii10103', 'afii10104',
+ 'afii10105', 'afii10106', 'afii10107', 'afii10108',
+ 'afii10109', 'section', 'afii10110', 'afii10193'
+ ],
+# Greek
+ 'ISO-8859-7' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'quoteleft', 'quoteright', 'sterling',
+ '.notdef', '.notdef', 'brokenbar', 'section',
+ 'dieresis', 'copyright', '.notdef', 'guillemotleft',
+ 'logicalnot', 'hyphen', '.notdef', 'afii00208',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'tonos', 'dieresistonos', 'Alphatonos', 'periodcentered',
+ 'Epsilontonos', 'Etatonos', 'Iotatonos', 'guillemotright',
+ 'Omicrontonos', 'onehalf', 'Upsilontonos', 'Omegatonos',
+ 'iotadieresistonos','Alpha', 'Beta', 'Gamma',
+ 'Delta', 'Epsilon', 'Zeta', 'Eta',
+ 'Theta', 'Iota', 'Kappa', 'Lambda',
+ 'Mu', 'Nu', 'Xi', 'Omicron',
+ 'Pi', 'Rho', '.notdef', 'Sigma',
+ 'Tau', 'Upsilon', 'Phi', 'Chi',
+ 'Psi', 'Omega', 'Iotadieresis', 'Upsilondieresis',
+ 'alphatonos', 'epsilontonos', 'etatonos', 'iotatonos',
+ 'upsilondieresistonos','alpha', 'beta', 'gamma',
+ 'delta', 'epsilon', 'zeta', 'eta',
+ 'theta', 'iota', 'kappa', 'lambda',
+ 'mu', 'nu', 'xi', 'omicron',
+ 'pi', 'rho', 'sigma1', 'sigma',
+ 'tau', 'upsilon', 'phi', 'chi',
+ 'psi', 'omega', 'iotadieresis', 'upsilondieresis',
+ 'omicrontonos', 'upsilontonos', 'omegatonos', '.notdef'
+ ],
+# Turkish
+ 'ISO-8859-9' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'currency', 'yen', 'brokenbar', 'section',
+ 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'acute', 'mu', 'paragraph', 'periodcentered',
+ 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'onequarter', 'onehalf', 'threequarters', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Gbreve', 'Ntilde', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Idotaccent', 'Scedilla', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'gbreve', 'ntilde', 'ograve', 'oacute',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'dotlessi', 'scedilla', 'ydieresis'
+ ],
+# Thai
+ 'ISO-8859-11' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'kokaithai', 'khokhaithai', 'khokhuatthai',
+ 'khokhwaithai', 'khokhonthai', 'khorakhangthai', 'ngonguthai',
+ 'chochanthai', 'chochingthai', 'chochangthai', 'sosothai',
+ 'chochoethai', 'yoyingthai', 'dochadathai', 'topatakthai',
+ 'thothanthai', 'thonangmonthothai','thophuthaothai', 'nonenthai',
+ 'dodekthai', 'totaothai', 'thothungthai', 'thothahanthai',
+ 'thothongthai', 'nonuthai', 'bobaimaithai', 'poplathai',
+ 'phophungthai', 'fofathai', 'phophanthai', 'fofanthai',
+ 'phosamphaothai', 'momathai', 'yoyakthai', 'roruathai',
+ 'ruthai', 'lolingthai', 'luthai', 'wowaenthai',
+ 'sosalathai', 'sorusithai', 'sosuathai', 'hohipthai',
+ 'lochulathai', 'oangthai', 'honokhukthai', 'paiyannoithai',
+ 'saraathai', 'maihanakatthai', 'saraaathai', 'saraamthai',
+ 'saraithai', 'saraiithai', 'sarauethai', 'saraueethai',
+ 'sarauthai', 'sarauuthai', 'phinthuthai', '.notdef',
+ '.notdef', '.notdef', '.notdef', 'bahtthai',
+ 'saraethai', 'saraaethai', 'saraothai', 'saraaimaimuanthai',
+ 'saraaimaimalaithai','lakkhangyaothai','maiyamokthai', 'maitaikhuthai',
+ 'maiekthai', 'maithothai', 'maitrithai', 'maichattawathai',
+ 'thanthakhatthai','nikhahitthai', 'yamakkanthai', 'fongmanthai',
+ 'zerothai', 'onethai', 'twothai', 'threethai',
+ 'fourthai', 'fivethai', 'sixthai', 'seventhai',
+ 'eightthai', 'ninethai', 'angkhankhuthai', 'khomutthai',
+ '.notdef', '.notdef', '.notdef', '.notdef'
+ ],
+# Western Europe
+ 'ISO-8859-15' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclamdown', 'cent', 'sterling',
+ 'Euro', 'yen', 'Scaron', 'section',
+ 'scaron', 'copyright', 'ordfeminine', 'guillemotleft',
+ 'logicalnot', 'hyphen', 'registered', 'macron',
+ 'degree', 'plusminus', 'twosuperior', 'threesuperior',
+ 'Zcaron', 'mu', 'paragraph', 'periodcentered',
+ 'zcaron', 'onesuperior', 'ordmasculine', 'guillemotright',
+ 'OE', 'oe', 'Ydieresis', 'questiondown',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Atilde',
+ 'Adieresis', 'Aring', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Eth', 'Ntilde', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply',
+ 'Oslash', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Yacute', 'Thorn', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'atilde',
+ 'adieresis', 'aring', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'eth', 'ntilde', 'ograve', 'oacute',
+ 'ocircumflex', 'otilde', 'odieresis', 'divide',
+ 'oslash', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'yacute', 'thorn', 'ydieresis'
+ ],
+# Central Europe
+ 'ISO-8859-16' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'Aogonek', 'aogonek', 'Lslash',
+ 'Euro', 'quotedblbase', 'Scaron', 'section',
+ 'scaron', 'copyright', 'Scommaaccent', 'guillemotleft',
+ 'Zacute', 'hyphen', 'zacute', 'Zdotaccent',
+ 'degree', 'plusminus', 'Ccaron', 'lslash',
+ 'Zcaron', 'quotedblright', 'paragraph', 'periodcentered',
+ 'zcaron', 'ccaron', 'scommaaccent', 'guillemotright',
+ 'OE', 'oe', 'Ydieresis', 'zdotaccent',
+ 'Agrave', 'Aacute', 'Acircumflex', 'Abreve',
+ 'Adieresis', 'Cacute', 'AE', 'Ccedilla',
+ 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis',
+ 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis',
+ 'Dcroat', 'Nacute', 'Ograve', 'Oacute',
+ 'Ocircumflex', 'Ohungarumlaut', 'Odieresis', 'Sacute',
+ 'Uhungarumlaut', 'Ugrave', 'Uacute', 'Ucircumflex',
+ 'Udieresis', 'Eogonek', 'Tcommaaccent', 'germandbls',
+ 'agrave', 'aacute', 'acircumflex', 'abreve',
+ 'adieresis', 'cacute', 'ae', 'ccedilla',
+ 'egrave', 'eacute', 'ecircumflex', 'edieresis',
+ 'igrave', 'iacute', 'icircumflex', 'idieresis',
+ 'dcroat', 'nacute', 'ograve', 'oacute',
+ 'ocircumflex', 'ohungarumlaut', 'odieresis', 'sacute',
+ 'uhungarumlaut', 'ugrave', 'uacute', 'ucircumflex',
+ 'udieresis', 'eogonek', 'tcommaaccent', 'ydieresis'
+ ],
+# Russian
+ 'KOI8-R' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'SF100000', 'SF110000', 'SF010000', 'SF030000',
+ 'SF020000', 'SF040000', 'SF080000', 'SF090000',
+ 'SF060000', 'SF070000', 'SF050000', 'upblock',
+ 'dnblock', 'block', 'lfblock', 'rtblock',
+ 'ltshade', 'shade', 'dkshade', 'integraltp',
+ 'filledbox', 'periodcentered', 'radical', 'approxequal',
+ 'lessequal', 'greaterequal', 'space', 'integralbt',
+ 'degree', 'twosuperior', 'periodcentered', 'divide',
+ 'SF430000', 'SF240000', 'SF510000', 'afii10071',
+ 'SF520000', 'SF390000', 'SF220000', 'SF210000',
+ 'SF250000', 'SF500000', 'SF490000', 'SF380000',
+ 'SF280000', 'SF270000', 'SF260000', 'SF360000',
+ 'SF370000', 'SF420000', 'SF190000', 'afii10023',
+ 'SF200000', 'SF230000', 'SF470000', 'SF480000',
+ 'SF410000', 'SF450000', 'SF460000', 'SF400000',
+ 'SF540000', 'SF530000', 'SF440000', 'copyright',
+ 'afii10096', 'afii10065', 'afii10066', 'afii10088',
+ 'afii10069', 'afii10070', 'afii10086', 'afii10068',
+ 'afii10087', 'afii10074', 'afii10075', 'afii10076',
+ 'afii10077', 'afii10078', 'afii10079', 'afii10080',
+ 'afii10081', 'afii10097', 'afii10082', 'afii10083',
+ 'afii10084', 'afii10085', 'afii10072', 'afii10067',
+ 'afii10094', 'afii10093', 'afii10073', 'afii10090',
+ 'afii10095', 'afii10091', 'afii10089', 'afii10092',
+ 'afii10048', 'afii10017', 'afii10018', 'afii10040',
+ 'afii10021', 'afii10022', 'afii10038', 'afii10020',
+ 'afii10039', 'afii10026', 'afii10027', 'afii10028',
+ 'afii10029', 'afii10030', 'afii10031', 'afii10032',
+ 'afii10033', 'afii10049', 'afii10034', 'afii10035',
+ 'afii10036', 'afii10037', 'afii10024', 'afii10019',
+ 'afii10046', 'afii10045', 'afii10025', 'afii10042',
+ 'afii10047', 'afii10043', 'afii10041', 'afii10044'
+ ],
+# Ukrainian
+ 'KOI8-U' => [
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ '.notdef', '.notdef', '.notdef', '.notdef',
+ 'space', 'exclam', 'quotedbl', 'numbersign',
+ 'dollar', 'percent', 'ampersand', 'quotesingle',
+ 'parenleft', 'parenright', 'asterisk', 'plus',
+ 'comma', 'hyphen', 'period', 'slash',
+ 'zero', 'one', 'two', 'three',
+ 'four', 'five', 'six', 'seven',
+ 'eight', 'nine', 'colon', 'semicolon',
+ 'less', 'equal', 'greater', 'question',
+ 'at', 'A', 'B', 'C',
+ 'D', 'E', 'F', 'G',
+ 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', 'bracketleft',
+ 'backslash', 'bracketright', 'asciicircum', 'underscore',
+ 'grave', 'a', 'b', 'c',
+ 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o',
+ 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w',
+ 'x', 'y', 'z', 'braceleft',
+ 'bar', 'braceright', 'asciitilde', '.notdef',
+ 'SF100000', 'SF110000', 'SF010000', 'SF030000',
+ 'SF020000', 'SF040000', 'SF080000', 'SF090000',
+ 'SF060000', 'SF070000', 'SF050000', 'upblock',
+ 'dnblock', 'block', 'lfblock', 'rtblock',
+ 'ltshade', 'shade', 'dkshade', 'integraltp',
+ 'filledbox', 'bullet', 'radical', 'approxequal',
+ 'lessequal', 'greaterequal', 'space', 'integralbt',
+ 'degree', 'twosuperior', 'periodcentered', 'divide',
+ 'SF430000', 'SF240000', 'SF510000', 'afii10071',
+ 'afii10101', 'SF390000', 'afii10103', 'afii10104',
+ 'SF250000', 'SF500000', 'SF490000', 'SF380000',
+ 'SF280000', 'afii10098', 'SF260000', 'SF360000',
+ 'SF370000', 'SF420000', 'SF190000', 'afii10023',
+ 'afii10053', 'SF230000', 'afii10055', 'afii10056',
+ 'SF410000', 'SF450000', 'SF460000', 'SF400000',
+ 'SF540000', 'afii10050', 'SF440000', 'copyright',
+ 'afii10096', 'afii10065', 'afii10066', 'afii10088',
+ 'afii10069', 'afii10070', 'afii10086', 'afii10068',
+ 'afii10087', 'afii10074', 'afii10075', 'afii10076',
+ 'afii10077', 'afii10078', 'afii10079', 'afii10080',
+ 'afii10081', 'afii10097', 'afii10082', 'afii10083',
+ 'afii10084', 'afii10085', 'afii10072', 'afii10067',
+ 'afii10094', 'afii10093', 'afii10073', 'afii10090',
+ 'afii10095', 'afii10091', 'afii10089', 'afii10092',
+ 'afii10048', 'afii10017', 'afii10018', 'afii10040',
+ 'afii10021', 'afii10022', 'afii10038', 'afii10020',
+ 'afii10039', 'afii10026', 'afii10027', 'afii10028',
+ 'afii10029', 'afii10030', 'afii10031', 'afii10032',
+ 'afii10033', 'afii10049', 'afii10034', 'afii10035',
+ 'afii10036', 'afii10037', 'afii10024', 'afii10019',
+ 'afii10046', 'afii10045', 'afii10025', 'afii10042',
+ 'afii10047', 'afii10043', 'afii10041', 'afii10044'
+ ]
+}
+
+def ReadAFM(file, map)
+
+ # Read a font metric file
+ a = IO.readlines(file)
+
+ raise "File no found: #{file}" if a.size == 0
+
+ widths = {}
+ fm = {}
+ fix = { 'Edot' => 'Edotaccent', 'edot' => 'edotaccent',
+ 'Idot' => 'Idotaccent',
+ 'Zdot' => 'Zdotaccent', 'zdot' => 'zdotaccent',
+ 'Odblacute' => 'Ohungarumlaut', 'odblacute' => 'ohungarumlaut',
+ 'Udblacute' => 'Uhungarumlaut', 'udblacute' => 'uhungarumlaut',
+ 'Gcedilla' => 'Gcommaaccent', 'gcedilla' => 'gcommaaccent',
+ 'Kcedilla' => 'Kcommaaccent', 'kcedilla' => 'kcommaaccent',
+ 'Lcedilla' => 'Lcommaaccent', 'lcedilla' => 'lcommaaccent',
+ 'Ncedilla' => 'Ncommaaccent', 'ncedilla' => 'ncommaaccent',
+ 'Rcedilla' => 'Rcommaaccent', 'rcedilla' => 'rcommaaccent',
+ 'Scedilla' => 'Scommaaccent',' scedilla' => 'scommaaccent',
+ 'Tcedilla' => 'Tcommaaccent',' tcedilla' => 'tcommaaccent',
+ 'Dslash' => 'Dcroat', 'dslash' => 'dcroat',
+ 'Dmacron' => 'Dcroat', 'dmacron' => 'dcroat',
+ 'combininggraveaccent' => 'gravecomb',
+ 'combininghookabove' => 'hookabovecomb',
+ 'combiningtildeaccent' => 'tildecomb',
+ 'combiningacuteaccent' => 'acutecomb',
+ 'combiningdotbelow' => 'dotbelowcomb',
+ 'dongsign' => 'dong'
+ }
+
+ a.each do |line|
+
+ e = line.rstrip.split(' ')
+ next if e.size < 2
+
+ code = e[0]
+ param = e[1]
+
+ if code == 'C' then
+
+ # Character metrics
+ cc = e[1].to_i
+ w = e[4]
+ gn = e[7]
+
+ gn = 'Euro' if gn[-4, 4] == '20AC'
+
+ if fix[gn] then
+
+ # Fix incorrect glyph name
+ 0.upto(map.size - 1) do |i|
+ if map[i] == fix[gn] then
+ map[i] = gn
+ end
+ end
+ end
+
+ if map.size == 0 then
+ # Symbolic font: use built-in encoding
+ widths[cc] = w
+ else
+ widths[gn] = w
+ fm['CapXHeight'] = e[13].to_i if gn == 'X'
+ end
+
+ fm['MissingWidth'] = w if gn == '.notdef'
+
+ elsif code == 'FontName' then
+ fm['FontName'] = param
+ elsif code == 'Weight' then
+ fm['Weight'] = param
+ elsif code == 'ItalicAngle' then
+ fm['ItalicAngle'] = param.to_f
+ elsif code == 'Ascender' then
+ fm['Ascender'] = param.to_i
+ elsif code == 'Descender' then
+ fm['Descender'] = param.to_i
+ elsif code == 'UnderlineThickness' then
+ fm['UnderlineThickness'] = param.to_i
+ elsif code == 'UnderlinePosition' then
+ fm['UnderlinePosition'] = param.to_i
+ elsif code == 'IsFixedPitch' then
+ fm['IsFixedPitch'] = (param == 'true')
+ elsif code == 'FontBBox' then
+ fm['FontBBox'] = "[#{e[1]},#{e[2]},#{e[3]},#{e[4]}]"
+ elsif code == 'CapHeight' then
+ fm['CapHeight'] = param.to_i
+ elsif code == 'StdVW' then
+ fm['StdVW'] = param.to_i
+ end
+ end
+
+ raise 'FontName not found' unless fm['FontName']
+
+ if map.size > 0 then
+ widths['.notdef'] = 600 unless widths['.notdef']
+
+ if (widths['Delta'] == nil) && widths['increment'] then
+ widths['Delta'] = widths['increment']
+ end
+
+ # Order widths according to map
+ 0.upto(255) do |i|
+ if widths[map[i]] == nil
+ puts "Warning: character #{map[i]} is missing"
+ widths[i] = widths['.notdef']
+ else
+ widths[i] = widths[map[i]]
+ end
+ end
+ end
+
+ fm['Widths'] = widths
+
+ return fm
+end
+
+def MakeFontDescriptor(fm, symbolic)
+
+ # Ascent
+ asc = fm['Ascender'] ? fm['Ascender'] : 1000
+ fd = "{\n 'Ascent' => '#{asc}'"
+
+ # Descent
+ desc = fm['Descender'] ? fm['Descender'] : -200
+ fd += ", 'Descent' => '#{desc}'"
+
+ # CapHeight
+ if fm['CapHeight'] then
+ ch = fm['CapHeight']
+ elsif fm['CapXHeight']
+ ch = fm['CapXHeight']
+ else
+ ch = asc
+ end
+ fd += ", 'CapHeight' => '#{ch}'"
+
+ # Flags
+ flags = 0
+
+ if fm['IsFixedPitch'] then
+ flags += 1 << 0
+ end
+
+ if symbolic then
+ flags += 1 << 2
+ else
+ flags += 1 << 5
+ end
+
+ if fm['ItalicAngle'] && (fm['ItalicAngle'] != 0) then
+ flags += 1 << 6
+ end
+
+ fd += ",\n 'Flags' => '#{flags}'"
+
+ # FontBBox
+ if fm['FontBBox'] then
+ fbb = fm['FontBBox'].gsub(/,/, ' ')
+ else
+ fbb = "[0 #{desc - 100} 1000 #{asc + 100}]"
+ end
+
+ fd += ", 'FontBBox' => '#{fbb}'"
+
+ # ItalicAngle
+ ia = fm['ItalicAngle'] ? fm['ItalicAngle'] : 0
+ fd += ",\n 'ItalicAngle' => '#{ia}'"
+
+ # StemV
+ if fm['StdVW'] then
+ stemv = fm['StdVW']
+ elsif fm['Weight'] && (/bold|black/i =~ fm['Weight'])
+ stemv = 120
+ else
+ stemv = 70
+ end
+
+ fd += ", 'StemV' => '#{stemv}'"
+
+ # MissingWidth
+ if fm['MissingWidth'] then
+ fd += ", 'MissingWidth' => '#{fm['MissingWidth']}'"
+ end
+
+ fd += "\n }"
+ return fd
+end
+
+def MakeWidthArray(fm)
+
+ # Make character width array
+ s = " [\n "
+
+ cw = fm['Widths']
+
+ 0.upto(255) do |i|
+ s += "%5d" % cw[i]
+ s += "," if i != 255
+ s += "\n " if (i % 8) == 7
+ end
+
+ s += ']'
+
+ return s
+end
+
+def MakeFontEncoding(map)
+
+ # Build differences from reference encoding
+ ref = Charencodings['cp1252']
+ s = ''
+ last = 0
+ 32.upto(255) do |i|
+ if map[i] != ref[i] then
+ if i != last + 1 then
+ s += i.to_s + ' '
+ end
+ last = i
+ s += '/' + map[i] + ' '
+ end
+ end
+ return s.rstrip
+end
+
+def ReadShort(f)
+ a = f.read(2).unpack('n')
+ return a[0]
+end
+
+def ReadLong(f)
+ a = f.read(4).unpack('N')
+ return a[0]
+end
+
+def CheckTTF(file)
+
+ rl = false
+ pp = false
+ e = false
+
+ # Check if font license allows embedding
+ File.open(file, 'rb') do |f|
+
+ # Extract number of tables
+ f.seek(4, IO::SEEK_CUR)
+ nb = ReadShort(f)
+ f.seek(6, IO::SEEK_CUR)
+
+ # Seek OS/2 table
+ found = false
+ 0.upto(nb - 1) do |i|
+ if f.read(4) == 'OS/2' then
+ found = true
+ break
+ end
+
+ f.seek(12, IO::SEEK_CUR)
+ end
+
+ if ! found then
+ return
+ end
+
+ f.seek(4, IO::SEEK_CUR)
+ offset = ReadLong(f)
+ f.seek(offset, IO::SEEK_SET)
+
+ # Extract fsType flags
+ f.seek(8, IO::SEEK_CUR)
+ fsType = ReadShort(f)
+
+ rl = (fsType & 0x02) != 0
+ pp = (fsType & 0x04) != 0
+ e = (fsType & 0x08) != 0
+ end
+
+ if rl && ( ! pp) && ( ! e) then
+ puts 'Warning: font license does not allow embedding'
+ end
+end
+
+#
+# fontfile: path to TTF file (or empty string if not to be embedded)
+# afmfile: path to AFM file
+# enc: font encoding (or empty string for symbolic fonts)
+# patch: optional patch for encoding
+# type : font type if $fontfile is empty
+#
+def MakeFont(fontfile, afmfile, enc = 'cp1252', patch = {}, type = 'TrueType')
+ # Generate a font definition file
+ if (enc != nil) && (enc != '') then
+ map = Charencodings[enc]
+ patch.each { |cc, gn| map[cc] = gn }
+ else
+ map = []
+ end
+
+ raise "Error: AFM file not found: #{afmfile}" unless File.exists?(afmfile)
+
+ fm = ReadAFM(afmfile, map)
+
+ if (enc != nil) && (enc != '') then
+ diff = MakeFontEncoding(map)
+ else
+ diff = ''
+ end
+
+ fd = MakeFontDescriptor(fm, (map.size == 0))
+
+ # Find font type
+ if fontfile then
+ ext = File.extname(fontfile).downcase.sub(/^\./, '')
+
+ if ext == 'ttf' then
+ type = 'TrueType'
+ elsif ext == 'pfb'
+ type = 'Type1'
+ else
+ raise "Error: unrecognized font file extension: #{ext}"
+ end
+ else
+ raise "Error: incorrect font type: #{type}" if (type != 'TrueType') && (type != 'Type1')
+ end
+ printf "type = #{type}\n"
+ # Start generation
+ s = "# #{fm['FontName']} font definition\n\n"
+ s += "module FontDef\n"
+ s += " def FontDef.type\n '#{type}'\n end\n"
+ s += " def FontDef.name\n '#{fm['FontName']}'\n end\n"
+ s += " def FontDef.desc\n #{fd}\n end\n"
+
+ if fm['UnderlinePosition'] == nil then
+ fm['UnderlinePosition'] = -100
+ end
+
+ if fm['UnderlineThickness'] == nil then
+ fm['UnderlineThickness'] = 50
+ end
+
+ s += " def FontDef.up\n #{fm['UnderlinePosition']}\n end\n"
+ s += " def FontDef.ut\n #{fm['UnderlineThickness']}\n end\n"
+
+ w = MakeWidthArray(fm)
+ s += " def FontDef.cw\n#{w}\n end\n"
+
+ s += " def FontDef.enc\n '#{enc}'\n end\n"
+ s += " def FontDef.diff\n #{(diff == nil) || (diff == '') ? 'nil' : '\'' + diff + '\''}\n end\n"
+
+ basename = File.basename(afmfile, '.*')
+
+ if fontfile then
+ # Embedded font
+ if ! File.exist?(fontfile) then
+ raise "Error: font file not found: #{fontfile}"
+ end
+
+ if type == 'TrueType' then
+ CheckTTF(fontfile)
+ end
+
+ file = ''
+ File.open(fontfile, 'rb') do |f|
+ file = f.read()
+ end
+
+ if type == 'Type1' then
+ # Find first two sections and discard third one
+ header = file[0] == 128
+ file = file[6, file.length - 6] if header
+
+ pos = file.index('eexec')
+ raise 'Error: font file does not seem to be valid Type1' if pos == nil
+
+ size1 = pos + 6
+
+ file = file[0, size1] + file[size1 + 6, file.length - (size1 + 6)] if header && file[size1] == 128
+
+ pos = file.index('00000000')
+ raise 'Error: font file does not seem to be valid Type1' if pos == nil
+
+ size2 = pos - size1
+ file = file[0, size1 + size2]
+ end
+
+ if require 'zlib' then
+ File.open(basename + '.z', 'wb') { |f| f.write(Zlib::Deflate.deflate(file)) }
+ s += " def FontDef.file\n '#{basename}.z'\n end\n"
+ puts "Font file compressed ('#{basename}.z')"
+ else
+ s += " def FontDef.file\n '#{File.basename(fontfile)}'\n end\n"
+ puts 'Notice: font file could not be compressed (zlib not available)'
+ end
+
+ if type == 'Type1' then
+ s += " def FontDef.size1\n '#{size1}'\n end\n"
+ s += " def FontDef.size2\n '#{size2}'\n end\n"
+ else
+ s += " def FontDef.originalsize\n '#{File.size(fontfile)}'\n end\n"
+ end
+
+ else
+ # Not embedded font
+ s += " def FontDef.file\n ''\n end\n"
+ end
+
+ s += "end\n"
+ File.open(basename + '.rb', 'w') { |file| file.write(s)}
+ puts "Font definition file generated (#{basename}.rb)"
+end
+
+
+if $0 == __FILE__ then
+ if ARGV.length >= 3 then
+ enc = ARGV[2]
+ else
+ enc = 'cp1252'
+ end
+
+ if ARGV.length >= 4 then
+ patch = ARGV[3]
+ else
+ patch = {}
+ end
+
+ if ARGV.length >= 5 then
+ type = ARGV[4]
+ else
+ type = 'TrueType'
+ end
+
+ MakeFont(ARGV[0], ARGV[1], enc, patch, type)
+end
--- /dev/null
+module RFPDF
+ COLOR_PALETTE = {
+ :black => [0x00, 0x00, 0x00],
+ :white => [0xff, 0xff, 0xff],
+ }.freeze
+
+ # Draw a line from (<tt>x1, y1</tt>) to (<tt>x2, y2</tt>).
+ #
+ # Options are:
+ # * <tt>:line_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:line_width</tt> - Default value is <tt>0.5</tt>.
+ #
+ # Example:
+ #
+ # draw_line(x1, y1, x1, y1+h, :line_color => ReportHelper::COLOR_PALETTE[:dark_blue], :line_width => 1)
+ #
+ def draw_line(x1, y1, x2, y2, options = {})
+ options[:line_color] ||= COLOR_PALETTE[:black]
+ options[:line_width] ||= 0.5
+ set_draw_color(options[:line_color])
+ SetLineWidth(options[:line_width])
+ Line(x1, y1, x2, y2)
+ end
+
+ # Draw a string of <tt>text</tt> at (<tt>x, y</tt>).
+ #
+ # Options are:
+ # * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:font_size</tt> - Default value is <tt>10</tt>.
+ # * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
+ #
+ # Example:
+ #
+ # draw_text(x, y, header_left, :font_size => 10)
+ #
+ def draw_text(x, y, text, options = {})
+ options[:font_color] ||= COLOR_PALETTE[:black]
+ options[:font_size] ||= 10
+ options[:font_style] ||= ''
+ set_text_color(options[:font_color])
+ SetFont('Arial', options[:font_style], options[:font_size])
+ SetXY(x, y)
+ Write(options[:font_size] + 4, text)
+ end
+
+ # Draw a block of <tt>text</tt> at (<tt>x, y</tt>) bounded by <tt>left_margin</tt> and <tt>right_margin</tt>. Both
+ # margins are measured from their corresponding edge.
+ #
+ # Options are:
+ # * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:font_size</tt> - Default value is <tt>10</tt>.
+ # * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
+ #
+ # Example:
+ #
+ # draw_text_block(left_margin, 85, "question", left_margin, 280,
+ # :font_color => ReportHelper::COLOR_PALETTE[:dark_blue],
+ # :font_size => 12,
+ # :font_style => 'I')
+ #
+ def draw_text_block(x, y, text, left_margin, right_margin, options = {})
+ options[:font_color] ||= COLOR_PALETTE[:black]
+ options[:font_size] ||= 10
+ options[:font_style] ||= ''
+ set_text_color(options[:font_color])
+ SetFont('Arial', options[:font_style], options[:font_size])
+ SetXY(x, y)
+ SetLeftMargin(left_margin)
+ SetRightMargin(right_margin)
+ Write(options[:font_size] + 4, text)
+ SetMargins(0,0,0)
+ end
+
+ # Draw a box at (<tt>x, y</tt>), <tt>w</tt> wide and <tt>h</tt> high.
+ #
+ # Options are:
+ # * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>1</tt>.
+ # * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
+ # * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
+ # * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
+ #
+ # Example:
+ #
+ # draw_box(x, y - 1, 38, 22)
+ #
+ def draw_box(x, y, w, h, options = {})
+ options[:border] ||= 1
+ options[:border_color] ||= COLOR_PALETTE[:black]
+ options[:border_width] ||= 0.5
+ options[:fill] ||= 1
+ options[:fill_color] ||= COLOR_PALETTE[:white]
+ SetLineWidth(options[:border_width])
+ set_draw_color(options[:border_color])
+ set_fill_color(options[:fill_color])
+ fd = ""
+ fd = "D" if options[:border] == 1
+ fd += "F" if options[:fill] == 1
+ Rect(x, y, w, h, fd)
+ end
+
+ # Draw a string of <tt>text</tt> at (<tt>x, y</tt>) in a box <tt>w</tt> wide and <tt>h</tt> high.
+ #
+ # Options are:
+ # * <tt>:align</tt> - Vertical alignment 'C' = center, 'L' = left, 'R' = right. Default value is <tt>'C'</tt>.
+ # * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>0</tt>.
+ # * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
+ # * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
+ # * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
+ # * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:font_size</tt> - Default value is nothing or <tt>8</tt>.
+ # * <tt>:font_style</tt> - 'B' = bold, 'I' = italic, 'U' = underline. Default value is nothing <tt>''</tt>.
+ # * <tt>:padding</tt> - Default value is nothing or <tt>2</tt>.
+ # * <tt>:valign</tt> - 'M' = middle, 'T' = top, 'B' = bottom. Default value is nothing or <tt>'M'</tt>.
+ #
+ # Example:
+ #
+ # draw_text_box(x, y - 1, 38, 22,
+ # "your_score_title",
+ # :fill => 0,
+ # :font_color => ReportHelper::COLOR_PALETTE[:blue],
+ # :font_line_spacing => 0,
+ # :font_style => "B",
+ # :valign => "M")
+ #
+ def draw_text_box(x, y, w, h, text, options = {})
+ options[:align] ||= 'C'
+ options[:border] ||= 0
+ options[:border_color] ||= COLOR_PALETTE[:black]
+ options[:border_width] ||= 0.5
+ options[:fill] ||= 1
+ options[:fill_color] ||= COLOR_PALETTE[:white]
+ options[:font_color] ||= COLOR_PALETTE[:black]
+ options[:font_size] ||= 8
+ options[:font_line_spacing] ||= options[:font_size] * 0.3
+ options[:font_style] ||= ''
+ options[:padding] ||= 2
+ options[:valign] ||= "M"
+ if options[:fill] == 1 or options[:border] == 1
+ draw_box(x, y, w, h, options)
+ end
+ SetMargins(0,0,0)
+ set_text_color(options[:font_color])
+ font_size = options[:font_size]
+ SetFont('Arial', options[:font_style], font_size)
+ font_size += options[:font_line_spacing]
+ case options[:valign]
+ when "B"
+ y -= options[:padding]
+ text = "\n" + text if text["\n"].nil?
+ when "T"
+ y += options[:padding]
+ end
+ SetXY(x, y)
+ if GetStringWidth(text) > w or not text["\n"].nil? or options[:valign] == "T"
+ font_size += options[:font_size] * 0.1
+ #TODO 2006-07-21 Level=1 - this is assuming a 2 line text
+ SetXY(x, y + ((h - (font_size * 2)) / 2)) if options[:valign] == "M"
+ MultiCell(w, font_size, text, 0, options[:align])
+ else
+ Cell(w, h, text, 0, 0, options[:align])
+ end
+ end
+
+ # Draw a string of <tt>text</tt> at (<tt>x, y</tt>) as a title.
+ #
+ # Options are:
+ # * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ # * <tt>:font_size</tt> - Default value is <tt>18</tt>.
+ # * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
+ #
+ # Example:
+ #
+ # draw_title(left_margin, 60,
+ # "title:",
+ # :font_color => ReportHelper::COLOR_PALETTE[:dark_blue])
+ #
+ def draw_title(x, y, title, options = {})
+ options[:font_color] ||= COLOR_PALETTE[:black]
+ options[:font_size] ||= 18
+ options[:font_style] ||= ''
+ set_text_color(options[:font_color])
+ SetFont('Arial', options[:font_style], options[:font_size])
+ SetXY(x, y)
+ Write(options[:font_size] + 2, title)
+ end
+
+ # Set the draw color. Default value is <tt>COLOR_PALETTE[:black]</tt>.
+ #
+ # Example:
+ #
+ # set_draw_color(ReportHelper::COLOR_PALETTE[:dark_blue])
+ #
+ def set_draw_color(color = COLOR_PALETTE[:black])
+ SetDrawColor(color[0], color[1], color[2])
+ end
+
+ # Set the fill color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
+ #
+ # Example:
+ #
+ # set_fill_color(ReportHelper::COLOR_PALETTE[:dark_blue])
+ #
+ def set_fill_color(color = COLOR_PALETTE[:white])
+ SetFillColor(color[0], color[1], color[2])
+ end
+
+ # Set the text color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
+ #
+ # Example:
+ #
+ # set_text_color(ReportHelper::COLOR_PALETTE[:dark_blue])
+ #
+ def set_text_color(color = COLOR_PALETTE[:black])
+ SetTextColor(color[0], color[1], color[2])
+ end
+
+ # Write a string containing html characters. Default value is <tt>COLOR_PALETTE[:white]</tt>.
+ #
+ # Options are:
+ # * <tt>:height</tt> - Line height. Default value is <tt>20</tt>.
+ #
+ # Example:
+ #
+ # write_html(html, :height => 12)
+ #
+ def write_html(html, options = {})
+ options[:height] ||= 20
+ #HTML parser
+ @href = nil
+ @style = {}
+ html.gsub!("\n",' ')
+ re = %r{ ( <!--.*?--> |
+ < (?:
+ [^<>"] +
+ |
+ " (?: \\. | [^\\"]+ ) * "
+ ) *
+ >
+ ) }xm
+
+ html.split(re).each do |value|
+ if "<" == value[0,1]
+ #Tag
+ if (value[1, 1] == '/')
+ close_tag(value[2..-2], options)
+ else
+ tag = value[1..-2]
+ open_tag(tag, options)
+ end
+ else
+ #Text
+ if @href
+ put_link(@href,value)
+ else
+ Write(options[:height], value)
+ end
+ end
+ end
+ end
+
+ def open_tag(tag, options = {}) #:nodoc:
+ #Opening tag
+ tag = tag.to_s.upcase
+ set_style(tag, true) if tag == 'B' or tag == 'I' or tag == 'U'
+ @href = options['HREF'] if tag == 'A'
+ Ln(options[:height]) if tag == 'BR'
+ end
+
+ def close_tag(tag, options = {}) #:nodoc:
+ #Closing tag
+ tag = tag.to_s.upcase
+ set_style(tag, false) if tag == 'B' or tag == 'I' or tag == 'U'
+ @href = '' if $tag == 'A'
+ end
+
+ def set_style(tag, enable = true) #:nodoc:
+ #Modify style and select corresponding font
+ style = ""
+ @style[tag] = enable
+ ['B','I','U'].each do |s|
+ style += s if not @style[s].nil? and @style[s]
+ end
+ SetFont('', style)
+ end
+
+ def put_link(url, txt) #:nodoc:
+ #Put a hyperlink
+ SetTextColor(0,0,255)
+ set_style('U',true)
+ Write(5, txt, url)
+ set_style('U',false)
+ SetTextColor(0)
+ end
+end
+
+# class FPDF
+# alias_method :set_margins , :SetMargins
+# alias_method :set_left_margin , :SetLeftMargin
+# alias_method :set_top_margin , :SetTopMargin
+# alias_method :set_right_margin , :SetRightMargin
+# alias_method :set_auto_pagebreak , :SetAutoPageBreak
+# alias_method :set_display_mode , :SetDisplayMode
+# alias_method :set_compression , :SetCompression
+# alias_method :set_title , :SetTitle
+# alias_method :set_subject , :SetSubject
+# alias_method :set_author , :SetAuthor
+# alias_method :set_keywords , :SetKeywords
+# alias_method :set_creator , :SetCreator
+# alias_method :set_draw_color , :SetDrawColor
+# alias_method :set_fill_color , :SetFillColor
+# alias_method :set_text_color , :SetTextColor
+# alias_method :set_line_width , :SetLineWidth
+# alias_method :set_font , :SetFont
+# alias_method :set_font_size , :SetFontSize
+# alias_method :set_link , :SetLink
+# alias_method :set_y , :SetY
+# alias_method :set_xy , :SetXY
+# alias_method :get_string_width , :GetStringWidth
+# alias_method :get_x , :GetX
+# alias_method :set_x , :SetX
+# alias_method :get_y , :GetY
+# alias_method :accept_pagev_break , :AcceptPageBreak
+# alias_method :add_font , :AddFont
+# alias_method :add_link , :AddLink
+# alias_method :add_page , :AddPage
+# alias_method :alias_nb_pages , :AliasNbPages
+# alias_method :cell , :Cell
+# alias_method :close , :Close
+# alias_method :error , :Error
+# alias_method :footer , :Footer
+# alias_method :header , :Header
+# alias_method :image , :Image
+# alias_method :line , :Line
+# alias_method :link , :Link
+# alias_method :ln , :Ln
+# alias_method :multi_cell , :MultiCell
+# alias_method :open , :Open
+# alias_method :Open , :open
+# alias_method :output , :Output
+# alias_method :page_no , :PageNo
+# alias_method :rect , :Rect
+# alias_method :text , :Text
+# alias_method :write , :Write
+# end
--- /dev/null
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+#
+# The MIT License
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# Thanks go out to Bruce Williams of codefluency who created RTex. This
+# template handler is modification of his work.
+#
+# Example Registration
+#
+# ActionView::Base::register_template_handler 'rfpdf', RFpdfView
+
+module RFPDF
+
+ class View
+ @@backward_compatibility_mode = false
+ cattr_accessor :backward_compatibility_mode
+
+ def initialize(action_view)
+ @action_view = action_view
+ # Override with @options_for_rfpdf Hash in your controller
+ @options = {
+ # Run through latex first? (for table of contents, etc)
+ :pre_process => false,
+ # Debugging mode; raises exception
+ :debug => false,
+ # Filename of pdf to generate
+ :file_name => "#{@action_view.controller.action_name}.pdf",
+ # Temporary Directory
+ :temp_dir => "#{File.expand_path(RAILS_ROOT)}/tmp"
+ }.merge(@action_view.controller.instance_eval{ @options_for_rfpdf } || {}).with_indifferent_access
+ end
+
+ def self.compilable?
+ false
+ end
+
+ def compilable?
+ self.class.compilable?
+ end
+
+ def render(template, local_assigns = {})
+ @pdf_name = "Default.pdf" if @pdf_name.nil?
+ unless @action_view.controller.headers["Content-Type"] == 'application/pdf'
+ @generate = true
+ @action_view.controller.headers["Content-Type"] = 'application/pdf'
+ @action_view.controller.headers["Content-disposition:"] = "inline; filename=\"#{@options[:file_name]}\""
+ end
+ assigns = @action_view.assigns.dup
+
+ if content_for_layout = @action_view.instance_variable_get("@content_for_layout")
+ assigns['content_for_layout'] = content_for_layout
+ end
+
+ result = @action_view.instance_eval do
+ assigns.each do |key,val|
+ instance_variable_set "@#{key}", val
+ end
+ local_assigns.each do |key,val|
+ class << self; self; end.send(:define_method,key){ val }
+ end
+ ERB.new(@@backward_compatibility_mode == true ? template : template.source).result(binding)
+ end
+ end
+
+ end
+
+end
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env ruby
\ No newline at end of file
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street,
+Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and
+distribute verbatim copies of this license document, but changing it is not
+allowed.
+
+ Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public
+License applies to most of the Free Software Foundation's software and to
+any other program whose authors commit to using it. (Some other Free
+Software Foundation software is covered by the GNU Lesser General Public
+License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you
+wish), that you receive source code or can get it if you want it, that you
+can change the software or use pieces of it in new free programs; and that
+you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to
+deny you these rights or to ask you to surrender the rights. These
+restrictions translate to certain responsibilities for you if you distribute
+copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or
+for a fee, you must give the recipients all the rights that you have. You
+must make sure that they, too, receive or can get the source code. And you
+must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2)
+offer you this license which gives you legal permission to copy, distribute
+and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If
+the software is modified by someone else and passed on, we want its
+recipients to know that what they have is not the original, so that any
+problems introduced by others will not reflect on the original authors'
+reputations.
+
+Finally, any free program is threatened constantly by software patents. We
+wish to avoid the danger that redistributors of a free program will
+individually obtain patent licenses, in effect making the program
+proprietary. To prevent this, we have made it clear that any patent must be
+licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+ placed by the copyright holder saying it may be distributed under the
+ terms of this General Public License. The "Program", below, refers to
+ any such program or work, and a "work based on the Program" means either
+ the Program or any derivative work under copyright law: that is to say, a
+ work containing the Program or a portion of it, either verbatim or with
+ modifications and/or translated into another language. (Hereinafter,
+ translation is included without limitation in the term "modification".)
+ Each licensee is addressed as "you".
+
+ Activities other than copying, distribution and modification are not
+ covered by this License; they are outside its scope. The act of running
+ the Program is not restricted, and the output from the Program is covered
+ only if its contents constitute a work based on the Program (independent
+ of having been made by running the Program). Whether that is true depends
+ on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code
+ as you receive it, in any medium, provided that you conspicuously and
+ appropriately publish on each copy an appropriate copyright notice and
+ disclaimer of warranty; keep intact all the notices that refer to this
+ License and to the absence of any warranty; and give any other recipients
+ of the Program a copy of this License along with the Program.
+
+ You may charge a fee for the physical act of transferring a copy, and you
+ may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it,
+ thus forming a work based on the Program, and copy and distribute such
+ modifications or work under the terms of Section 1 above, provided that
+ you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part
+ thereof, to be licensed as a whole at no charge to all third parties
+ under the terms of this License.
+
+ c) If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use
+ in the most ordinary way, to print or display an announcement
+ including an appropriate copyright notice and a notice that there is
+ no warranty (or else, saying that you provide a warranty) and that
+ users may redistribute the program under these conditions, and telling
+ the user how to view a copy of this License. (Exception: if the
+ Program itself is interactive but does not normally print such an
+ announcement, your work based on the Program is not required to print
+ an announcement.)
+
+ These requirements apply to the modified work as a whole. If
+ identifiable sections of that work are not derived from the Program, and
+ can be reasonably considered independent and separate works in
+ themselves, then this License, and its terms, do not apply to those
+ sections when you distribute them as separate works. But when you
+ distribute the same sections as part of a whole which is a work based on
+ the Program, the distribution of the whole must be on the terms of this
+ License, whose permissions for other licensees extend to the entire
+ whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest
+ your rights to work written entirely by you; rather, the intent is to
+ exercise the right to control the distribution of derivative or
+ collective works based on the Program.
+
+ In addition, mere aggregation of another work not based on the Program
+ with the Program (or with a work based on the Program) on a volume of a
+ storage or distribution medium does not bring the other work under the
+ scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+ Section 2) in object code or executable form under the terms of Sections
+ 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2
+ above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of
+ physically performing source distribution, a complete machine-readable
+ copy of the corresponding source code, to be distributed under the
+ terms of Sections 1 and 2 above on a medium customarily used for
+ software interchange; or,
+
+ c) Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed
+ only for noncommercial distribution and only if you received the
+ program in object code or executable form with such an offer, in
+ accord with Subsection b above.)
+
+ The source code for a work means the preferred form of the work for
+ making modifications to it. For an executable work, complete source code
+ means all the source code for all modules it contains, plus any
+ associated interface definition files, plus the scripts used to control
+ compilation and installation of the executable. However, as a special
+ exception, the source code distributed need not include anything that is
+ normally distributed (in either source or binary form) with the major
+ components (compiler, kernel, and so on) of the operating system on which
+ the executable runs, unless that component itself accompanies the
+ executable.
+
+ If distribution of executable or object code is made by offering access
+ to copy from a designated place, then offering equivalent access to copy
+ the source code from the same place counts as distribution of the source
+ code, even though third parties are not compelled to copy the source
+ along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+ expressly provided under this License. Any attempt otherwise to copy,
+ modify, sublicense or distribute the Program is void, and will
+ automatically terminate your rights under this License. However, parties
+ who have received copies, or rights, from you under this License will not
+ have their licenses terminated so long as such parties remain in full
+ compliance.
+
+5. You are not required to accept this License, since you have not signed
+ it. However, nothing else grants you permission to modify or distribute
+ the Program or its derivative works. These actions are prohibited by law
+ if you do not accept this License. Therefore, by modifying or
+ distributing the Program (or any work based on the Program), you indicate
+ your acceptance of this License to do so, and all its terms and
+ conditions for copying, distributing or modifying the Program or works
+ based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+ Program), the recipient automatically receives a license from the
+ original licensor to copy, distribute or modify the Program subject to
+ these terms and conditions. You may not impose any further restrictions
+ on the recipients' exercise of the rights granted herein. You are not
+ responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+ infringement or for any other reason (not limited to patent issues),
+ conditions are imposed on you (whether by court order, agreement or
+ otherwise) that contradict the conditions of this License, they do not
+ excuse you from the conditions of this License. If you cannot distribute
+ so as to satisfy simultaneously your obligations under this License and
+ any other pertinent obligations, then as a consequence you may not
+ distribute the Program at all. For example, if a patent license would
+ not permit royalty-free redistribution of the Program by all those who
+ receive copies directly or indirectly through you, then the only way you
+ could satisfy both it and this License would be to refrain entirely from
+ distribution of the Program.
+
+ If any portion of this section is held invalid or unenforceable under any
+ particular circumstance, the balance of the section is intended to apply
+ and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any
+ patents or other property right claims or to contest validity of any such
+ claims; this section has the sole purpose of protecting the integrity of
+ the free software distribution system, which is implemented by public
+ license practices. Many people have made generous contributions to the
+ wide range of software distributed through that system in reliance on
+ consistent application of that system; it is up to the author/donor to
+ decide if he or she is willing to distribute software through any other
+ system and a licensee cannot impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to be
+ a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain
+ countries either by patents or by copyrighted interfaces, the original
+ copyright holder who places the Program under this License may add an
+ explicit geographical distribution limitation excluding those countries,
+ so that distribution is permitted only in or among countries not thus
+ excluded. In such case, this License incorporates the limitation as if
+ written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of
+ the General Public License from time to time. Such new versions will be
+ similar in spirit to the present version, but may differ in detail to
+ address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Program
+ specifies a version number of this License which applies to it and "any
+ later version", you have the option of following the terms and conditions
+ either of that version or of any later version published by the Free
+ Software Foundation. If the Program does not specify a version number of
+ this License, you may choose any version ever published by the Free
+ Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+ whose distribution conditions are different, write to the author to ask
+ for permission. For software which is copyrighted by the Free Software
+ Foundation, write to the Free Software Foundation; we sometimes make
+ exceptions for this. Our decision will be guided by the two goals of
+ preserving the free status of all derivatives of our free software and
+ of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+ THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+ EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
+ YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+ NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
+ DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
+ DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+ (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+ INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
+ THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
+ OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
--- /dev/null
+= Net::LDAP Changelog
+
+== Net::LDAP 0.0.4: August 15, 2006
+* Undeprecated Net::LDAP#modify. Thanks to Justin Forder for
+ providing the rationale for this.
+* Added a much-expanded set of special characters to the parser
+ for RFC-2254 filters. Thanks to Andre Nathan.
+* Changed Net::LDAP#search so you can pass it a filter in string form.
+ The conversion to a Net::LDAP::Filter now happens automatically.
+* Implemented Net::LDAP#bind_as (preliminary and subject to change).
+ Thanks for Simon Claret for valuable suggestions and for helping test.
+* Fixed bug in Net::LDAP#open that was preventing #open from being
+ called more than one on a given Net::LDAP object.
+
+== Net::LDAP 0.0.3: July 26, 2006
+* Added simple TLS encryption.
+ Thanks to Garett Shulman for suggestions and for helping test.
+
+== Net::LDAP 0.0.2: July 12, 2006
+* Fixed malformation in distro tarball and gem.
+* Improved documentation.
+* Supported "paged search control."
+* Added a range of API improvements.
+* Thanks to Andre Nathan, andre@digirati.com.br, for valuable
+ suggestions.
+* Added support for LE and GE search filters.
+* Added support for Search referrals.
+* Fixed a regression with openldap 2.2.x and higher caused
+ by the introduction of RFC-2696 controls. Thanks to Andre
+ Nathan for reporting the problem.
+* Added support for RFC-2254 filter syntax.
+
+== Net::LDAP 0.0.1: May 1, 2006
+* Initial release.
+* Client functionality is near-complete, although the APIs
+ are not guaranteed and may change depending on feedback
+ from the community.
+* We're internally working on a Ruby-based implementation
+ of a full-featured, production-quality LDAP server,
+ which will leverage the underlying LDAP and BER functionality
+ in Net::LDAP.
+* Please tell us if you would be interested in seeing a public
+ release of the LDAP server.
+* Grateful acknowledgement to Austin Ziegler, who reviewed
+ this code and provided the release framework, including
+ minitar.
+
+#--
+# Net::LDAP for Ruby.
+# http://rubyforge.org/projects/net-ldap/
+# Copyright (C) 2006 by Francis Cianfrocca
+#
+# Available under the same terms as Ruby. See LICENCE in the main
+# distribution for full licensing information.
+#
+# $Id: ChangeLog,v 1.17.2.4 2005/09/09 12:36:42 austin Exp $
+#++
+# vim: sts=2 sw=2 ts=4 et ai tw=77
--- /dev/null
+Net::LDAP is copyrighted free software by Francis Cianfrocca
+<garbagecat10@gmail.com>. You can redistribute it and/or modify it under either
+the terms of the GPL (see the file COPYING), or the conditions below:
+
+1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+2. You may modify your copy of the software in any way, provided that you do
+ at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise make them
+ Freely Available, such as by posting said modifications to Usenet or
+ an equivalent medium, or by allowing the author to include your
+ modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) rename any non-standard executables so the names do not conflict with
+ standard executables, which must also be provided.
+
+ d) make other distribution arrangements with the author.
+
+3. You may distribute the software in object code or executable form,
+ provided that you do at least ONE of the following:
+
+ a) distribute the executables and library files of the software, together
+ with instructions (in the manual page or equivalent) on where to get
+ the original distribution.
+
+ b) accompany the distribution with the machine-readable source of the
+ software.
+
+ c) give non-standard executables non-standard names, with instructions on
+ where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+4. You may modify and include the part of the software into any other
+ software (possibly commercial). But some files in the distribution are
+ not written by the author, so that they are not under this terms.
+
+ They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
+ files under the ./missing directory. See each file for the copying
+ condition.
+
+5. The scripts and library files supplied as input to or produced as output
+ from the software do not automatically fall under the copyright of the
+ software, but belong to whomever generated them, and may be sold
+ commercially, and may be aggregated with this software.
+
+6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
+ WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
--- /dev/null
+= Net::LDAP for Ruby
+Net::LDAP is an LDAP support library written in pure Ruby. It supports all
+LDAP client features, and a subset of server features as well.
+
+Homepage:: http://rubyforge.org/projects/net-ldap/
+Copyright:: (C) 2006 by Francis Cianfrocca
+
+Original developer: Francis Cianfrocca
+Contributions by Austin Ziegler gratefully acknowledged.
+
+== LICENCE NOTES
+Please read the file LICENCE for licensing restrictions on this library. In
+the simplest terms, this library is available under the same terms as Ruby
+itself.
+
+== Requirements
+Net::LDAP requires Ruby 1.8.2 or better.
+
+== Documentation
+See Net::LDAP for documentation and usage samples.
+
+#--
+# Net::LDAP for Ruby.
+# http://rubyforge.org/projects/net-ldap/
+# Copyright (C) 2006 by Francis Cianfrocca
+#
+# Available under the same terms as Ruby. See LICENCE in the main
+# distribution for full licensing information.
+#
+# $Id: README 141 2006-07-12 10:37:37Z blackhedd $
+#++
+# vim: sts=2 sw=2 ts=4 et ai tw=77
--- /dev/null
+# $Id: ber.rb 142 2006-07-26 12:20:33Z blackhedd $
+#
+# NET::BER
+# Mixes ASN.1/BER convenience methods into several standard classes.
+# Also provides BER parsing functionality.
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+#
+
+
+
+
+module Net
+
+ module BER
+
+ class BerError < Exception; end
+
+
+ # This module is for mixing into IO and IO-like objects.
+ module BERParser
+
+ # The order of these follows the class-codes in BER.
+ # Maybe this should have been a hash.
+ TagClasses = [:universal, :application, :context_specific, :private]
+
+ BuiltinSyntax = {
+ :universal => {
+ :primitive => {
+ 1 => :boolean,
+ 2 => :integer,
+ 4 => :string,
+ 10 => :integer,
+ },
+ :constructed => {
+ 16 => :array,
+ 17 => :array
+ }
+ }
+ }
+
+ #
+ # read_ber
+ # TODO: clean this up so it works properly with partial
+ # packets coming from streams that don't block when
+ # we ask for more data (like StringIOs). At it is,
+ # this can throw TypeErrors and other nasties.
+ #
+ def read_ber syntax=nil
+ return nil if (StringIO == self.class) and eof?
+
+ id = getc # don't trash this value, we'll use it later
+ tag = id & 31
+ tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" )
+ tagclass = TagClasses[ id >> 6 ]
+ encoding = (id & 0x20 != 0) ? :constructed : :primitive
+
+ n = getc
+ lengthlength,contentlength = if n <= 127
+ [1,n]
+ else
+ j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc}
+ [1 + (n & 127), j]
+ end
+
+ newobj = read contentlength
+
+ objtype = nil
+ [syntax, BuiltinSyntax].each {|syn|
+ if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag]
+ objtype = ot[tag]
+ break
+ end
+ }
+
+ obj = case objtype
+ when :boolean
+ newobj != "\000"
+ when :string
+ (newobj || "").dup
+ when :integer
+ j = 0
+ newobj.each_byte {|b| j = (j << 8) + b}
+ j
+ when :array
+ seq = []
+ sio = StringIO.new( newobj || "" )
+ # Interpret the subobject, but note how the loop
+ # is built: nil ends the loop, but false (a valid
+ # BER value) does not!
+ while (e = sio.read_ber(syntax)) != nil
+ seq << e
+ end
+ seq
+ else
+ raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" )
+ end
+
+ # Add the identifier bits into the object if it's a String or an Array.
+ # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway.
+ obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end"
+ obj
+
+ end
+
+ end # module BERParser
+ end # module BER
+
+end # module Net
+
+
+class IO
+ include Net::BER::BERParser
+end
+
+require "stringio"
+class StringIO
+ include Net::BER::BERParser
+end
+
+begin
+ require 'openssl'
+ class OpenSSL::SSL::SSLSocket
+ include Net::BER::BERParser
+ end
+rescue LoadError
+# Ignore LoadError.
+# DON'T ignore NameError, which means the SSLSocket class
+# is somehow unavailable on this implementation of Ruby's openssl.
+# This may be WRONG, however, because we don't yet know how Ruby's
+# openssl behaves on machines with no OpenSSL library. I suppose
+# it's possible they do not fail to require 'openssl' but do not
+# create the classes. So this code is provisional.
+# Also, you might think that OpenSSL::SSL::SSLSocket inherits from
+# IO so we'd pick it up above. But you'd be wrong.
+end
+
+class String
+ def read_ber syntax=nil
+ StringIO.new(self).read_ber(syntax)
+ end
+end
+
+
+
+#----------------------------------------------
+
+
+class FalseClass
+ #
+ # to_ber
+ #
+ def to_ber
+ "\001\001\000"
+ end
+end
+
+
+class TrueClass
+ #
+ # to_ber
+ #
+ def to_ber
+ "\001\001\001"
+ end
+end
+
+
+
+class Fixnum
+ #
+ # to_ber
+ #
+ def to_ber
+ i = [self].pack('w')
+ [2, i.length].pack("CC") + i
+ end
+
+ #
+ # to_ber_enumerated
+ #
+ def to_ber_enumerated
+ i = [self].pack('w')
+ [10, i.length].pack("CC") + i
+ end
+
+ #
+ # to_ber_length_encoding
+ #
+ def to_ber_length_encoding
+ if self <= 127
+ [self].pack('C')
+ else
+ i = [self].pack('N').sub(/^[\0]+/,"")
+ [0x80 + i.length].pack('C') + i
+ end
+ end
+
+end # class Fixnum
+
+
+class Bignum
+
+ def to_ber
+ i = [self].pack('w')
+ i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" )
+ [2, i.length].pack("CC") + i
+ end
+
+end
+
+
+
+class String
+ #
+ # to_ber
+ # A universal octet-string is tag number 4,
+ # but others are possible depending on the context, so we
+ # let the caller give us one.
+ # The preferred way to do this in user code is via to_ber_application_sring
+ # and to_ber_contextspecific.
+ #
+ def to_ber code = 4
+ [code].pack('C') + length.to_ber_length_encoding + self
+ end
+
+ #
+ # to_ber_application_string
+ #
+ def to_ber_application_string code
+ to_ber( 0x40 + code )
+ end
+
+ #
+ # to_ber_contextspecific
+ #
+ def to_ber_contextspecific code
+ to_ber( 0x80 + code )
+ end
+
+end # class String
+
+
+
+class Array
+ #
+ # to_ber_appsequence
+ # An application-specific sequence usually gets assigned
+ # a tag that is meaningful to the particular protocol being used.
+ # This is different from the universal sequence, which usually
+ # gets a tag value of 16.
+ # Now here's an interesting thing: We're adding the X.690
+ # "application constructed" code at the top of the tag byte (0x60),
+ # but some clients, notably ldapsearch, send "context-specific
+ # constructed" (0xA0). The latter would appear to violate RFC-1777,
+ # but what do I know? We may need to change this.
+ #
+
+ def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end
+ def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end
+ def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end
+ def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end
+ def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end
+
+ private
+ def to_ber_seq_internal code
+ s = self.to_s
+ [code].pack('C') + s.length.to_ber_length_encoding + s
+ end
+
+end # class Array
+
+
--- /dev/null
+# $Id: ldap.rb 154 2006-08-15 09:35:43Z blackhedd $
+#
+# Net::LDAP for Ruby
+#
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Written and maintained by Francis Cianfrocca, gmail: garbagecat10.
+#
+# This program is free software.
+# You may re-distribute and/or modify this program under the same terms
+# as Ruby itself: Ruby Distribution License or GNU General Public License.
+#
+#
+# See Net::LDAP for documentation and usage samples.
+#
+
+
+require 'socket'
+require 'ostruct'
+
+begin
+ require 'openssl'
+ $net_ldap_openssl_available = true
+rescue LoadError
+end
+
+require 'net/ber'
+require 'net/ldap/pdu'
+require 'net/ldap/filter'
+require 'net/ldap/dataset'
+require 'net/ldap/psw'
+require 'net/ldap/entry'
+
+
+module Net
+
+
+ # == Net::LDAP
+ #
+ # This library provides a pure-Ruby implementation of the
+ # LDAP client protocol, per RFC-2251.
+ # It can be used to access any server which implements the
+ # LDAP protocol.
+ #
+ # Net::LDAP is intended to provide full LDAP functionality
+ # while hiding the more arcane aspects
+ # the LDAP protocol itself, and thus presenting as Ruby-like
+ # a programming interface as possible.
+ #
+ # == Quick-start for the Impatient
+ # === Quick Example of a user-authentication against an LDAP directory:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = your_server_ip_address
+ # ldap.port = 389
+ # ldap.auth "joe_user", "opensesame"
+ # if ldap.bind
+ # # authentication succeeded
+ # else
+ # # authentication failed
+ # end
+ #
+ #
+ # === Quick Example of a search against an LDAP directory:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new :host => server_ip_address,
+ # :port => 389,
+ # :auth => {
+ # :method => :simple,
+ # :username => "cn=manager,dc=example,dc=com",
+ # :password => "opensesame"
+ # }
+ #
+ # filter = Net::LDAP::Filter.eq( "cn", "George*" )
+ # treebase = "dc=example,dc=com"
+ #
+ # ldap.search( :base => treebase, :filter => filter ) do |entry|
+ # puts "DN: #{entry.dn}"
+ # entry.each do |attribute, values|
+ # puts " #{attribute}:"
+ # values.each do |value|
+ # puts " --->#{value}"
+ # end
+ # end
+ # end
+ #
+ # p ldap.get_operation_result
+ #
+ #
+ # == A Brief Introduction to LDAP
+ #
+ # We're going to provide a quick, informal introduction to LDAP
+ # terminology and
+ # typical operations. If you're comfortable with this material, skip
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment
+ # of this material, we recommend you start with the various IETF and ITU
+ # standards that relate to LDAP.
+ #
+ # === Entities
+ # LDAP is an Internet-standard protocol used to access directory servers.
+ # The basic search unit is the <i>entity,</i> which corresponds to
+ # a person or other domain-specific object.
+ # A directory service which supports the LDAP protocol typically
+ # stores information about a number of entities.
+ #
+ # === Principals
+ # LDAP servers are typically used to access information about people,
+ # but also very often about such items as printers, computers, and other
+ # resources. To reflect this, LDAP uses the term <i>entity,</i> or less
+ # commonly, <i>principal,</i> to denote its basic data-storage unit.
+ #
+ #
+ # === Distinguished Names
+ # In LDAP's view of the world,
+ # an entity is uniquely identified by a globally-unique text string
+ # called a <i>Distinguished Name,</i> originally defined in the X.400
+ # standards from which LDAP is ultimately derived.
+ # Much like a DNS hostname, a DN is a "flattened" text representation
+ # of a string of tree nodes. Also like DNS (and unlike Java package
+ # names), a DN expresses a chain of tree-nodes written from left to right
+ # in order from the most-resolved node to the most-general one.
+ #
+ # If you know the DN of a person or other entity, then you can query
+ # an LDAP-enabled directory for information (attributes) about the entity.
+ # Alternatively, you can query the directory for a list of DNs matching
+ # a set of criteria that you supply.
+ #
+ # === Attributes
+ #
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
+ # Information about the entity is stored as a set of <i>Attributes.</i>
+ # An attribute is a text string which is associated with zero or more
+ # values. Most LDAP-enabled directories store a well-standardized
+ # range of attributes, and constrain their values according to standard
+ # rules.
+ #
+ # A good example of an attribute is <tt>sn,</tt> which stands for "Surname."
+ # This attribute is generally used to store a person's surname, or last name.
+ # Most directories enforce the standard convention that
+ # an entity's <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP
+ # jargon, that means that <tt>sn</tt> must be <i>present</i> and
+ # <i>single-valued.</i>
+ #
+ # Another attribute is <tt>mail,</tt> which is used to store email addresses.
+ # (No, there is no attribute called "email," perhaps because X.400 terminology
+ # predates the invention of the term <i>email.</i>) <tt>mail</tt> differs
+ # from <tt>sn</tt> in that most directories permit any number of values for the
+ # <tt>mail</tt> attribute, including zero.
+ #
+ #
+ # === Tree-Base
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
+ # contains authoritative attribute data for a set of DNs corresponding
+ # to a specific sub-tree of the (notional) global directory tree.
+ # This subtree is generally configured into a directory server when it is
+ # created. It matters for this discussion because most servers will not
+ # allow you to query them unless you specify a correct tree-base.
+ #
+ # Let's say you work for the engineering department of Big Company, Inc.,
+ # whose internet domain is bigcompany.com. You may find that your departmental
+ # directory is stored in a server with a defined tree-base of
+ # ou=engineering,dc=bigcompany,dc=com
+ # You will need to supply this string as the <i>tree-base</i> when querying this
+ # directory. (Ou is a very old X.400 term meaning "organizational unit."
+ # Dc is a more recent term meaning "domain component.")
+ #
+ # === LDAP Versions
+ # (stub, discuss v2 and v3)
+ #
+ # === LDAP Operations
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and #rename.
+ # ==== Bind
+ # #bind supplies a user's authentication credentials to a server, which in turn verifies
+ # or rejects them. There is a range of possibilities for credentials, but most directories
+ # support a simple username and password authentication.
+ #
+ # Taken by itself, #bind can be used to authenticate a user against information
+ # stored in a directory, for example to permit or deny access to some other resource.
+ # In terms of the other LDAP operations, most directories require a successful #bind to
+ # be performed before the other operations will be permitted. Some servers permit certain
+ # operations to be performed with an "anonymous" binding, meaning that no credentials are
+ # presented by the user. (We're glossing over a lot of platform-specific detail here.)
+ #
+ # ==== Search
+ # Calling #search against the directory involves specifying a treebase, a set of <i>search filters,</i>
+ # and a list of attribute values.
+ # The filters specify ranges of possible values for particular attributes. Multiple
+ # filters can be joined together with AND, OR, and NOT operators.
+ # A server will respond to a #search by returning a list of matching DNs together with a
+ # set of attribute values for each entity, depending on what attributes the search requested.
+ #
+ # ==== Add
+ # #add specifies a new DN and an initial set of attribute values. If the operation
+ # succeeds, a new entity with the corresponding DN and attributes is added to the directory.
+ #
+ # ==== Modify
+ # #modify specifies an entity DN, and a list of attribute operations. #modify is used to change
+ # the attribute values stored in the directory for a particular entity.
+ # #modify may add or delete attributes (which are lists of values) or it change attributes by
+ # adding to or deleting from their values.
+ # Net::LDAP provides three easier methods to modify an entry's attribute values:
+ # #add_attribute, #replace_attribute, and #delete_attribute.
+ #
+ # ==== Delete
+ # #delete specifies an entity DN. If it succeeds, the entity and all its attributes
+ # is removed from the directory.
+ #
+ # ==== Rename (or Modify RDN)
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP protocol. It responds to
+ # the often-arising need to change the DN of an entity without discarding its attribute values.
+ # In earlier LDAP versions, the only way to do this was to delete the whole entity and add it
+ # again with a different DN.
+ #
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN," which is the left-most
+ # part of the DN string. If successful, #rename changes the entity DN so that its left-most
+ # node corresponds to the new RDN given in the request. (RDN, or "relative distinguished name,"
+ # denotes a single tree-node as expressed in a DN, which is a chain of tree nodes.)
+ #
+ # == How to use Net::LDAP
+ #
+ # To access Net::LDAP functionality in your Ruby programs, start by requiring
+ # the library:
+ #
+ # require 'net/ldap'
+ #
+ # If you installed the Gem version of Net::LDAP, and depending on your version of
+ # Ruby and rubygems, you _may_ also need to require rubygems explicitly:
+ #
+ # require 'rubygems'
+ # require 'net/ldap'
+ #
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
+ # The constructor for this object takes arguments specifying the network location
+ # (address and port) of the LDAP server, and also the binding (authentication)
+ # credentials, typically a username and password.
+ # Given an object of class Net:LDAP, you can then perform LDAP operations by calling
+ # instance methods on the object. These are documented with usage examples below.
+ #
+ # The Net::LDAP library is designed to be very disciplined about how it makes network
+ # connections to servers. This is different from many of the standard native-code
+ # libraries that are provided on most platforms, which share bloodlines with the
+ # original Netscape/Michigan LDAP client implementations. These libraries sought to
+ # insulate user code from the workings of the network. This is a good idea of course,
+ # but the practical effect has been confusing and many difficult bugs have been caused
+ # by the opacity of the native libraries, and their variable behavior across platforms.
+ #
+ # In general, Net::LDAP instance methods which invoke server operations make a connection
+ # to the server when the method is called. They execute the operation (typically binding first)
+ # and then disconnect from the server. The exception is Net::LDAP#open, which makes a connection
+ # to the server and then keeps it open while it executes a user-supplied block. Net::LDAP#open
+ # closes the connection on completion of the block.
+ #
+
+ class LDAP
+
+ class LdapError < Exception; end
+
+ VERSION = "0.0.4"
+
+
+ SearchScope_BaseObject = 0
+ SearchScope_SingleLevel = 1
+ SearchScope_WholeSubtree = 2
+ SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree]
+
+ AsnSyntax = {
+ :application => {
+ :constructed => {
+ 0 => :array, # BindRequest
+ 1 => :array, # BindResponse
+ 2 => :array, # UnbindRequest
+ 3 => :array, # SearchRequest
+ 4 => :array, # SearchData
+ 5 => :array, # SearchResult
+ 6 => :array, # ModifyRequest
+ 7 => :array, # ModifyResponse
+ 8 => :array, # AddRequest
+ 9 => :array, # AddResponse
+ 10 => :array, # DelRequest
+ 11 => :array, # DelResponse
+ 12 => :array, # ModifyRdnRequest
+ 13 => :array, # ModifyRdnResponse
+ 14 => :array, # CompareRequest
+ 15 => :array, # CompareResponse
+ 16 => :array, # AbandonRequest
+ 19 => :array, # SearchResultReferral
+ 24 => :array, # Unsolicited Notification
+ }
+ },
+ :context_specific => {
+ :primitive => {
+ 0 => :string, # password
+ 1 => :string, # Kerberos v4
+ 2 => :string, # Kerberos v5
+ },
+ :constructed => {
+ 0 => :array, # RFC-2251 Control
+ 3 => :array, # Seach referral
+ }
+ }
+ }
+
+ DefaultHost = "127.0.0.1"
+ DefaultPort = 389
+ DefaultAuth = {:method => :anonymous}
+ DefaultTreebase = "dc=com"
+
+
+ ResultStrings = {
+ 0 => "Success",
+ 1 => "Operations Error",
+ 2 => "Protocol Error",
+ 3 => "Time Limit Exceeded",
+ 4 => "Size Limit Exceeded",
+ 12 => "Unavailable crtical extension",
+ 16 => "No Such Attribute",
+ 17 => "Undefined Attribute Type",
+ 20 => "Attribute or Value Exists",
+ 32 => "No Such Object",
+ 34 => "Invalid DN Syntax",
+ 48 => "Invalid DN Syntax",
+ 48 => "Inappropriate Authentication",
+ 49 => "Invalid Credentials",
+ 50 => "Insufficient Access Rights",
+ 51 => "Busy",
+ 52 => "Unavailable",
+ 53 => "Unwilling to perform",
+ 65 => "Object Class Violation",
+ 68 => "Entry Already Exists"
+ }
+
+
+ module LdapControls
+ PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
+ end
+
+
+ #
+ # LDAP::result2string
+ #
+ def LDAP::result2string code # :nodoc:
+ ResultStrings[code] || "unknown result (#{code})"
+ end
+
+
+ attr_accessor :host, :port, :base
+
+
+ # Instantiate an object of type Net::LDAP to perform directory operations.
+ # This constructor takes a Hash containing arguments, all of which are either optional or may be specified later with other methods as described below. The following arguments
+ # are supported:
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
+ # * :port => the LDAP server's TCP port (default 389)
+ # * :auth => a Hash containing authorization parameters. Currently supported values include:
+ # {:method => :anonymous} and
+ # {:method => :simple, :username => your_user_name, :password => your_password }
+ # The password parameter may be a Proc that returns a String.
+ # * :base => a default treebase parameter for searches performed against the LDAP server. If you don't give this value, then each call to #search must specify a treebase parameter. If you do give this value, then it will be used in subsequent calls to #search that do not specify a treebase. If you give a treebase value in any particular call to #search, that value will override any treebase value you give here.
+ # * :encryption => specifies the encryption to be used in communicating with the LDAP server. The value is either a Hash containing additional parameters, or the Symbol :simple_tls, which is equivalent to specifying the Hash {:method => :simple_tls}. There is a fairly large range of potential values that may be given for this parameter. See #encryption for details.
+ #
+ # Instantiating a Net::LDAP object does <i>not</i> result in network traffic to
+ # the LDAP server. It simply stores the connection and binding parameters in the
+ # object.
+ #
+ def initialize args = {}
+ @host = args[:host] || DefaultHost
+ @port = args[:port] || DefaultPort
+ @verbose = false # Make this configurable with a switch on the class.
+ @auth = args[:auth] || DefaultAuth
+ @base = args[:base] || DefaultTreebase
+ encryption args[:encryption] # may be nil
+
+ if pr = @auth[:password] and pr.respond_to?(:call)
+ @auth[:password] = pr.call
+ end
+
+ # This variable is only set when we are created with LDAP::open.
+ # All of our internal methods will connect using it, or else
+ # they will create their own.
+ @open_connection = nil
+ end
+
+ # Convenience method to specify authentication credentials to the LDAP
+ # server. Currently supports simple authentication requiring
+ # a username and password.
+ #
+ # Observe that on most LDAP servers,
+ # the username is a complete DN. However, with A/D, it's often possible
+ # to give only a user-name rather than a complete DN. In the latter
+ # case, beware that many A/D servers are configured to permit anonymous
+ # (uncredentialled) binding, and will silently accept your binding
+ # as anonymous if you give an unrecognized username. This is not usually
+ # what you want. (See #get_operation_result.)
+ #
+ # <b>Important:</b> The password argument may be a Proc that returns a string.
+ # This makes it possible for you to write client programs that solicit
+ # passwords from users or from other data sources without showing them
+ # in your code or on command lines.
+ #
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = server_ip_address
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", "your_psw"
+ #
+ # Alternatively (with a password block):
+ #
+ # require 'net/ldap'
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = server_ip_address
+ # psw = proc { your_psw_function }
+ # ldap.authenticate "cn=Your Username,cn=Users,dc=example,dc=com", psw
+ #
+ def authenticate username, password
+ password = password.call if password.respond_to?(:call)
+ @auth = {:method => :simple, :username => username, :password => password}
+ end
+
+ alias_method :auth, :authenticate
+
+ # Convenience method to specify encryption characteristics for connections
+ # to LDAP servers. Called implicitly by #new and #open, but may also be called
+ # by user code if desired.
+ # The single argument is generally a Hash (but see below for convenience alternatives).
+ # This implementation is currently a stub, supporting only a few encryption
+ # alternatives. As additional capabilities are added, more configuration values
+ # will be added here.
+ #
+ # Currently, the only supported argument is {:method => :simple_tls}.
+ # (Equivalently, you may pass the symbol :simple_tls all by itself, without
+ # enclosing it in a Hash.)
+ #
+ # The :simple_tls encryption method encrypts <i>all</i> communications with the LDAP
+ # server.
+ # It completely establishes SSL/TLS encryption with the LDAP server
+ # before any LDAP-protocol data is exchanged.
+ # There is no plaintext negotiation and no special encryption-request controls
+ # are sent to the server.
+ # <i>The :simple_tls option is the simplest, easiest way to encrypt communications
+ # between Net::LDAP and LDAP servers.</i>
+ # It's intended for cases where you have an implicit level of trust in the authenticity
+ # of the LDAP server. No validation of the LDAP server's SSL certificate is
+ # performed. This means that :simple_tls will not produce errors if the LDAP
+ # server's encryption certificate is not signed by a well-known Certification
+ # Authority.
+ # If you get communications or protocol errors when using this option, check
+ # with your LDAP server administrator. Pay particular attention to the TCP port
+ # you are connecting to. It's impossible for an LDAP server to support plaintext
+ # LDAP communications and <i>simple TLS</i> connections on the same port.
+ # The standard TCP port for unencrypted LDAP connections is 389, but the standard
+ # port for simple-TLS encrypted connections is 636. Be sure you are using the
+ # correct port.
+ #
+ # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP control,
+ # which will enable encrypted communications on the same TCP port used for
+ # unencrypted connections.]</i>
+ #
+ def encryption args
+ if args == :simple_tls
+ args = {:method => :simple_tls}
+ end
+ @encryption = args
+ end
+
+
+ # #open takes the same parameters as #new. #open makes a network connection to the
+ # LDAP server and then passes a newly-created Net::LDAP object to the caller-supplied block.
+ # Within the block, you can call any of the instance methods of Net::LDAP to
+ # perform operations against the LDAP directory. #open will perform all the
+ # operations in the user-supplied block on the same network connection, which
+ # will be closed automatically when the block finishes.
+ #
+ # # (PSEUDOCODE)
+ # auth = {:method => :simple, :username => username, :password => password}
+ # Net::LDAP.open( :host => ipaddress, :port => 389, :auth => auth ) do |ldap|
+ # ldap.search( ... )
+ # ldap.add( ... )
+ # ldap.modify( ... )
+ # end
+ #
+ def LDAP::open args
+ ldap1 = LDAP.new args
+ ldap1.open {|ldap| yield ldap }
+ end
+
+ # Returns a meaningful result any time after
+ # a protocol operation (#bind, #search, #add, #modify, #rename, #delete)
+ # has completed.
+ # It returns an #OpenStruct containing an LDAP result code (0 means success),
+ # and a human-readable string.
+ # unless ldap.bind
+ # puts "Result: #{ldap.get_operation_result.code}"
+ # puts "Message: #{ldap.get_operation_result.message}"
+ # end
+ #
+ def get_operation_result
+ os = OpenStruct.new
+ if @result
+ os.code = @result
+ else
+ os.code = 0
+ end
+ os.message = LDAP.result2string( os.code )
+ os
+ end
+
+
+ # Opens a network connection to the server and then
+ # passes <tt>self</tt> to the caller-supplied block. The connection is
+ # closed when the block completes. Used for executing multiple
+ # LDAP operations without requiring a separate network connection
+ # (and authentication) for each one.
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This will
+ # be done for you automatically.
+ # For an even simpler approach, see the class method Net::LDAP#open.
+ #
+ # # (PSEUDOCODE)
+ # auth = {:method => :simple, :username => username, :password => password}
+ # ldap = Net::LDAP.new( :host => ipaddress, :port => 389, :auth => auth )
+ # ldap.open do |ldap|
+ # ldap.search( ... )
+ # ldap.add( ... )
+ # ldap.modify( ... )
+ # end
+ #--
+ # First we make a connection and then a binding, but we don't
+ # do anything with the bind results.
+ # We then pass self to the caller's block, where he will execute
+ # his LDAP operations. Of course they will all generate auth failures
+ # if the bind was unsuccessful.
+ def open
+ raise LdapError.new( "open already in progress" ) if @open_connection
+ @open_connection = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ @open_connection.bind @auth
+ yield self
+ @open_connection.close
+ @open_connection = nil
+ end
+
+
+ # Searches the LDAP directory for directory entries.
+ # Takes a hash argument with parameters. Supported parameters include:
+ # * :base (a string specifying the tree-base for the search);
+ # * :filter (an object of type Net::LDAP::Filter, defaults to objectclass=*);
+ # * :attributes (a string or array of strings specifying the LDAP attributes to return from the server);
+ # * :return_result (a boolean specifying whether to return a result set).
+ # * :attributes_only (a boolean flag, defaults false)
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject, Net::LDAP::SearchScope_SingleLevel, Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
+ #
+ # #search queries the LDAP server and passes <i>each entry</i> to the
+ # caller-supplied block, as an object of type Net::LDAP::Entry.
+ # If the search returns 1000 entries, the block will
+ # be called 1000 times. If the search returns no entries, the block will
+ # not be called.
+ #
+ #--
+ # ORIGINAL TEXT, replaced 04May06.
+ # #search returns either a result-set or a boolean, depending on the
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
+ # a result set, which is a hash. Each key in the hash is a string specifying
+ # the DN of an entry. The corresponding value for each key is a Net::LDAP::Entry object.
+ # If you request a result set and #search fails with an error, it will return nil.
+ # Call #get_operation_result to get the error information returned by
+ # the LDAP server.
+ #++
+ # #search returns either a result-set or a boolean, depending on the
+ # value of the <tt>:return_result</tt> argument. The default behavior is to return
+ # a result set, which is an Array of objects of class Net::LDAP::Entry.
+ # If you request a result set and #search fails with an error, it will return nil.
+ # Call #get_operation_result to get the error information returned by
+ # the LDAP server.
+ #
+ # When <tt>:return_result => false,</tt> #search will
+ # return only a Boolean, to indicate whether the operation succeeded. This can improve performance
+ # with very large result sets, because the library can discard each entry from memory after
+ # your block processes it.
+ #
+ #
+ # treebase = "dc=example,dc=com"
+ # filter = Net::LDAP::Filter.eq( "mail", "a*.com" )
+ # attrs = ["mail", "cn", "sn", "objectclass"]
+ # ldap.search( :base => treebase, :filter => filter, :attributes => attrs, :return_result => false ) do |entry|
+ # puts "DN: #{entry.dn}"
+ # entry.each do |attr, values|
+ # puts ".......#{attr}:"
+ # values.each do |value|
+ # puts " #{value}"
+ # end
+ # end
+ # end
+ #
+ #--
+ # This is a re-implementation of search that replaces the
+ # original one (now renamed searchx and possibly destined to go away).
+ # The difference is that we return a dataset (or nil) from the
+ # call, and pass _each entry_ as it is received from the server
+ # to the caller-supplied block. This will probably make things
+ # far faster as we can do useful work during the network latency
+ # of the search. The downside is that we have no access to the
+ # whole set while processing the blocks, so we can't do stuff
+ # like sort the DNs until after the call completes.
+ # It's also possible that this interacts badly with server timeouts.
+ # We'll have to ensure that something reasonable happens if
+ # the caller has processed half a result set when we throw a timeout
+ # error.
+ # Another important difference is that we return a result set from
+ # this method rather than a T/F indication.
+ # Since this can be very heavy-weight, we define an argument flag
+ # that the caller can set to suppress the return of a result set,
+ # if he's planning to process every entry as it comes from the server.
+ #
+ # REINTERPRETED the result set, 04May06. Originally this was a hash
+ # of entries keyed by DNs. But let's get away from making users
+ # handle DNs. Change it to a plain array. Eventually we may
+ # want to return a Dataset object that delegates to an internal
+ # array, so we can provide sort methods and what-not.
+ #
+ def search args = {}
+ args[:base] ||= @base
+ result_set = (args and args[:return_result] == false) ? nil : []
+
+ if @open_connection
+ @result = @open_connection.search( args ) {|entry|
+ result_set << entry if result_set
+ yield( entry ) if block_given?
+ }
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.search( args ) {|entry|
+ result_set << entry if result_set
+ yield( entry ) if block_given?
+ }
+ end
+ conn.close
+ end
+
+ @result == 0 and result_set
+ end
+
+ # #bind connects to an LDAP server and requests authentication
+ # based on the <tt>:auth</tt> parameter passed to #open or #new.
+ # It takes no parameters.
+ #
+ # User code does not need to call #bind directly. It will be called
+ # implicitly by the library whenever you invoke an LDAP operation,
+ # such as #search or #add.
+ #
+ # It is useful, however, to call #bind in your own code when the
+ # only operation you intend to perform against the directory is
+ # to validate a login credential. #bind returns true or false
+ # to indicate whether the binding was successful. Reasons for
+ # failure include malformed or unrecognized usernames and
+ # incorrect passwords. Use #get_operation_result to find out
+ # what happened in case of failure.
+ #
+ # Here's a typical example using #bind to authenticate a
+ # credential which was (perhaps) solicited from the user of a
+ # web site:
+ #
+ # require 'net/ldap'
+ # ldap = Net::LDAP.new
+ # ldap.host = your_server_ip_address
+ # ldap.port = 389
+ # ldap.auth your_user_name, your_user_password
+ # if ldap.bind
+ # # authentication succeeded
+ # else
+ # # authentication failed
+ # p ldap.get_operation_result
+ # end
+ #
+ # You don't have to create a new instance of Net::LDAP every time
+ # you perform a binding in this way. If you prefer, you can cache the Net::LDAP object
+ # and re-use it to perform subsequent bindings, <i>provided</i> you call
+ # #auth to specify a new credential before calling #bind. Otherwise, you'll
+ # just re-authenticate the previous user! (You don't need to re-set
+ # the values of #host and #port.) As noted in the documentation for #auth,
+ # the password parameter can be a Ruby Proc instead of a String.
+ #
+ #--
+ # If there is an @open_connection, then perform the bind
+ # on it. Otherwise, connect, bind, and disconnect.
+ # The latter operation is obviously useful only as an auth check.
+ #
+ def bind auth=@auth
+ if @open_connection
+ @result = @open_connection.bind auth
+ else
+ conn = Connection.new( :host => @host, :port => @port , :encryption => @encryption)
+ @result = conn.bind @auth
+ conn.close
+ end
+
+ @result == 0
+ end
+
+ #
+ # #bind_as is for testing authentication credentials.
+ #
+ # As described under #bind, most LDAP servers require that you supply a complete DN
+ # as a binding-credential, along with an authenticator such as a password.
+ # But for many applications (such as authenticating users to a Rails application),
+ # you often don't have a full DN to identify the user. You usually get a simple
+ # identifier like a username or an email address, along with a password.
+ # #bind_as allows you to authenticate these user-identifiers.
+ #
+ # #bind_as is a combination of a search and an LDAP binding. First, it connects and
+ # binds to the directory as normal. Then it searches the directory for an entry
+ # corresponding to the email address, username, or other string that you supply.
+ # If the entry exists, then #bind_as will <b>re-bind</b> as that user with the
+ # password (or other authenticator) that you supply.
+ #
+ # #bind_as takes the same parameters as #search, <i>with the addition of an
+ # authenticator.</i> Currently, this authenticator must be <tt>:password</tt>.
+ # Its value may be either a String, or a +proc+ that returns a String.
+ # #bind_as returns +false+ on failure. On success, it returns a result set,
+ # just as #search does. This result set is an Array of objects of
+ # type Net::LDAP::Entry. It contains the directory attributes corresponding to
+ # the user. (Just test whether the return value is logically true, if you don't
+ # need this additional information.)
+ #
+ # Here's how you would use #bind_as to authenticate an email address and password:
+ #
+ # require 'net/ldap'
+ #
+ # user,psw = "joe_user@yourcompany.com", "joes_psw"
+ #
+ # ldap = Net::LDAP.new
+ # ldap.host = "192.168.0.100"
+ # ldap.port = 389
+ # ldap.auth "cn=manager,dc=yourcompany,dc=com", "topsecret"
+ #
+ # result = ldap.bind_as(
+ # :base => "dc=yourcompany,dc=com",
+ # :filter => "(mail=#{user})",
+ # :password => psw
+ # )
+ # if result
+ # puts "Authenticated #{result.first.dn}"
+ # else
+ # puts "Authentication FAILED."
+ # end
+ def bind_as args={}
+ result = false
+ open {|me|
+ rs = search args
+ if rs and rs.first and dn = rs.first.dn
+ password = args[:password]
+ password = password.call if password.respond_to?(:call)
+ result = rs if bind :method => :simple, :username => dn, :password => password
+ end
+ }
+ result
+ end
+
+
+ # Adds a new entry to the remote LDAP server.
+ # Supported arguments:
+ # :dn :: Full DN of the new entry
+ # :attributes :: Attributes of the new entry.
+ #
+ # The attributes argument is supplied as a Hash keyed by Strings or Symbols
+ # giving the attribute name, and mapping to Strings or Arrays of Strings
+ # giving the actual attribute values. Observe that most LDAP directories
+ # enforce schema constraints on the attributes contained in entries.
+ # #add will fail with a server-generated error if your attributes violate
+ # the server-specific constraints.
+ # Here's an example:
+ #
+ # dn = "cn=George Smith,ou=people,dc=example,dc=com"
+ # attr = {
+ # :cn => "George Smith",
+ # :objectclass => ["top", "inetorgperson"],
+ # :sn => "Smith",
+ # :mail => "gsmith@example.com"
+ # }
+ # Net::LDAP.open (:host => host) do |ldap|
+ # ldap.add( :dn => dn, :attributes => attr )
+ # end
+ #
+ def add args
+ if @open_connection
+ @result = @open_connection.add( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption)
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.add( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+
+ # Modifies the attribute values of a particular entry on the LDAP directory.
+ # Takes a hash with arguments. Supported arguments are:
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
+ # :operations :: (the modifications to be performed, detailed next)
+ #
+ # This method returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result.
+ #
+ # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
+ # provide simpler interfaces to this functionality.
+ #
+ # The LDAP protocol provides a full and well thought-out set of operations
+ # for changing the values of attributes, but they are necessarily somewhat complex
+ # and not always intuitive. If these instructions are confusing or incomplete,
+ # please send us email or create a bug report on rubyforge.
+ #
+ # The :operations parameter to #modify takes an array of operation-descriptors.
+ # Each individual operation is specified in one element of the array, and
+ # most LDAP servers will attempt to perform the operations in order.
+ #
+ # Each of the operations appearing in the Array must itself be an Array
+ # with exactly three elements:
+ # an operator:: must be :add, :replace, or :delete
+ # an attribute name:: the attribute name (string or symbol) to modify
+ # a value:: either a string or an array of strings.
+ #
+ # The :add operator will, unsurprisingly, add the specified values to
+ # the specified attribute. If the attribute does not already exist,
+ # :add will create it. Most LDAP servers will generate an error if you
+ # try to add a value that already exists.
+ #
+ # :replace will erase the current value(s) for the specified attribute,
+ # if there are any, and replace them with the specified value(s).
+ #
+ # :delete will remove the specified value(s) from the specified attribute.
+ # If you pass nil, an empty string, or an empty array as the value parameter
+ # to a :delete operation, the _entire_ _attribute_ will be deleted, along
+ # with all of its values.
+ #
+ # For example:
+ #
+ # dn = "mail=modifyme@example.com,ou=people,dc=example,dc=com"
+ # ops = [
+ # [:add, :mail, "aliasaddress@example.com"],
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
+ # [:delete, :sn, nil]
+ # ]
+ # ldap.modify :dn => dn, :operations => ops
+ #
+ # <i>(This example is contrived since you probably wouldn't add a mail
+ # value right before replacing the whole attribute, but it shows that order
+ # of execution matters. Also, many LDAP servers won't let you delete SN
+ # because that would be a schema violation.)</i>
+ #
+ # It's essential to keep in mind that if you specify more than one operation in
+ # a call to #modify, most LDAP servers will attempt to perform all of the operations
+ # in the order you gave them.
+ # This matters because you may specify operations on the
+ # same attribute which must be performed in a certain order.
+ #
+ # Most LDAP servers will _stop_ processing your modifications if one of them
+ # causes an error on the server (such as a schema-constraint violation).
+ # If this happens, you will probably get a result code from the server that
+ # reflects only the operation that failed, and you may or may not get extended
+ # information that will tell you which one failed. #modify has no notion
+ # of an atomic transaction. If you specify a chain of modifications in one
+ # call to #modify, and one of them fails, the preceding ones will usually
+ # not be "rolled back," resulting in a partial update. This is a limitation
+ # of the LDAP protocol, not of Net::LDAP.
+ #
+ # The lack of transactional atomicity in LDAP means that you're usually
+ # better off using the convenience methods #add_attribute, #replace_attribute,
+ # and #delete_attribute, which are are wrappers over #modify. However, certain
+ # LDAP servers may provide concurrency semantics, in which the several operations
+ # contained in a single #modify call are not interleaved with other
+ # modification-requests received simultaneously by the server.
+ # It bears repeating that this concurrency does _not_ imply transactional
+ # atomicity, which LDAP does not provide.
+ #
+ def modify args
+ if @open_connection
+ @result = @open_connection.modify( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.modify( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+
+ # Add a value to an attribute.
+ # Takes the full DN of the entry to modify,
+ # the name (Symbol or String) of the attribute, and the value (String or
+ # Array). If the attribute does not exist (and there are no schema violations),
+ # #add_attribute will create it with the caller-specified values.
+ # If the attribute already exists (and there are no schema violations), the
+ # caller-specified values will be _added_ to the values already present.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #replace_attribute and #delete_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
+ #
+ def add_attribute dn, attribute, value
+ modify :dn => dn, :operations => [[:add, attribute, value]]
+ end
+
+ # Replace the value of an attribute.
+ # #replace_attribute can be thought of as equivalent to calling #delete_attribute
+ # followed by #add_attribute. It takes the full DN of the entry to modify,
+ # the name (Symbol or String) of the attribute, and the value (String or
+ # Array). If the attribute does not exist, it will be created with the
+ # caller-specified value(s). If the attribute does exist, its values will be
+ # _discarded_ and replaced with the caller-specified values.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
+ #
+ def replace_attribute dn, attribute, value
+ modify :dn => dn, :operations => [[:replace, attribute, value]]
+ end
+
+ # Delete an attribute and all its values.
+ # Takes the full DN of the entry to modify, and the
+ # name (Symbol or String) of the attribute to delete.
+ #
+ # Returns True or False to indicate whether the operation
+ # succeeded or failed, with extended information available by calling
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
+ #
+ # dn = "cn=modifyme,dc=example,dc=com"
+ # ldap.delete_attribute dn, :mail
+ #
+ def delete_attribute dn, attribute
+ modify :dn => dn, :operations => [[:delete, attribute, nil]]
+ end
+
+
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
+ # _Documentation_ _stub_
+ #
+ def rename args
+ if @open_connection
+ @result = @open_connection.rename( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.rename( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+ # modify_rdn is an alias for #rename.
+ def modify_rdn args
+ rename args
+ end
+
+ # Delete an entry from the LDAP directory.
+ # Takes a hash of arguments.
+ # The only supported argument is :dn, which must
+ # give the complete DN of the entry to be deleted.
+ # Returns True or False to indicate whether the delete
+ # succeeded. Extended status information is available by
+ # calling #get_operation_result.
+ #
+ # dn = "mail=deleteme@example.com,ou=people,dc=example,dc=com"
+ # ldap.delete :dn => dn
+ #
+ def delete args
+ if @open_connection
+ @result = @open_connection.delete( args )
+ else
+ @result = 0
+ conn = Connection.new( :host => @host, :port => @port, :encryption => @encryption )
+ if (@result = conn.bind( args[:auth] || @auth )) == 0
+ @result = conn.delete( args )
+ end
+ conn.close
+ end
+ @result == 0
+ end
+
+ end # class LDAP
+
+
+
+ class LDAP
+ # This is a private class used internally by the library. It should not be called by user code.
+ class Connection # :nodoc:
+
+ LdapVersion = 3
+
+
+ #--
+ # initialize
+ #
+ def initialize server
+ begin
+ @conn = TCPsocket.new( server[:host], server[:port] )
+ rescue
+ raise LdapError.new( "no connection to server" )
+ end
+
+ if server[:encryption]
+ setup_encryption server[:encryption]
+ end
+
+ yield self if block_given?
+ end
+
+
+ #--
+ # Helper method called only from new, and only after we have a successfully-opened
+ # @conn instance variable, which is a TCP connection.
+ # Depending on the received arguments, we establish SSL, potentially replacing
+ # the value of @conn accordingly.
+ # Don't generate any errors here if no encryption is requested.
+ # DO raise LdapError objects if encryption is requested and we have trouble setting
+ # it up. That includes if OpenSSL is not set up on the machine. (Question:
+ # how does the Ruby OpenSSL wrapper react in that case?)
+ # DO NOT filter exceptions raised by the OpenSSL library. Let them pass back
+ # to the user. That should make it easier for us to debug the problem reports.
+ # Presumably (hopefully?) that will also produce recognizable errors if someone
+ # tries to use this on a machine without OpenSSL.
+ #
+ # The simple_tls method is intended as the simplest, stupidest, easiest solution
+ # for people who want nothing more than encrypted comms with the LDAP server.
+ # It doesn't do any server-cert validation and requires nothing in the way
+ # of key files and root-cert files, etc etc.
+ # OBSERVE: WE REPLACE the value of @conn, which is presumed to be a connected
+ # TCPsocket object.
+ #
+ def setup_encryption args
+ case args[:method]
+ when :simple_tls
+ raise LdapError.new("openssl unavailable") unless $net_ldap_openssl_available
+ ctx = OpenSSL::SSL::SSLContext.new
+ @conn = OpenSSL::SSL::SSLSocket.new(@conn, ctx)
+ @conn.connect
+ @conn.sync_close = true
+ # additional branches requiring server validation and peer certs, etc. go here.
+ else
+ raise LdapError.new( "unsupported encryption method #{args[:method]}" )
+ end
+ end
+
+ #--
+ # close
+ # This is provided as a convenience method to make
+ # sure a connection object gets closed without waiting
+ # for a GC to happen. Clients shouldn't have to call it,
+ # but perhaps it will come in handy someday.
+ def close
+ @conn.close
+ @conn = nil
+ end
+
+ #--
+ # next_msgid
+ #
+ def next_msgid
+ @msgid ||= 0
+ @msgid += 1
+ end
+
+
+ #--
+ # bind
+ #
+ def bind auth
+ user,psw = case auth[:method]
+ when :anonymous
+ ["",""]
+ when :simple
+ [auth[:username] || auth[:dn], auth[:password]]
+ end
+ raise LdapError.new( "invalid binding information" ) unless (user && psw)
+
+ msgid = next_msgid.to_ber
+ request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
+ request_pkt = [msgid, request].to_ber_sequence
+ @conn.write request_pkt
+
+ (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" )
+ pdu.result_code
+ end
+
+ #--
+ # search
+ # Alternate implementation, this yields each search entry to the caller
+ # as it are received.
+ # TODO, certain search parameters are hardcoded.
+ # TODO, if we mis-parse the server results or the results are wrong, we can block
+ # forever. That's because we keep reading results until we get a type-5 packet,
+ # which might never come. We need to support the time-limit in the protocol.
+ #--
+ # WARNING: this code substantially recapitulates the searchx method.
+ #
+ # 02May06: Well, I added support for RFC-2696-style paged searches.
+ # This is used on all queries because the extension is marked non-critical.
+ # As far as I know, only A/D uses this, but it's required for A/D. Otherwise
+ # you won't get more than 1000 results back from a query.
+ # This implementation is kindof clunky and should probably be refactored.
+ # Also, is it my imagination, or are A/Ds the slowest directory servers ever???
+ #
+ def search args = {}
+ search_filter = (args && args[:filter]) || Filter.eq( "objectclass", "*" )
+ search_filter = Filter.construct(search_filter) if search_filter.is_a?(String)
+ search_base = (args && args[:base]) || "dc=example,dc=com"
+ search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber}
+ return_referrals = args && args[:return_referrals] == true
+
+ attributes_only = (args and args[:attributes_only] == true)
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
+ raise LdapError.new( "invalid search scope" ) unless SearchScopes.include?(scope)
+
+ # An interesting value for the size limit would be close to A/D's built-in
+ # page limit of 1000 records, but openLDAP newer than version 2.2.0 chokes
+ # on anything bigger than 126. You get a silent error that is easily visible
+ # by running slapd in debug mode. Go figure.
+ rfc2696_cookie = [126, ""]
+ result_code = 0
+
+ loop {
+ # should collect this into a private helper to clarify the structure
+
+ request = [
+ search_base.to_ber,
+ scope.to_ber_enumerated,
+ 0.to_ber_enumerated,
+ 0.to_ber,
+ 0.to_ber,
+ attributes_only.to_ber,
+ search_filter.to_ber,
+ search_attributes.to_ber_sequence
+ ].to_ber_appsequence(3)
+
+ controls = [
+ [
+ LdapControls::PagedResults.to_ber,
+ false.to_ber, # criticality MUST be false to interoperate with normal LDAPs.
+ rfc2696_cookie.map{|v| v.to_ber}.to_ber_sequence.to_s.to_ber
+ ].to_ber_sequence
+ ].to_ber_contextspecific(0)
+
+ pkt = [next_msgid.to_ber, request, controls].to_ber_sequence
+ @conn.write pkt
+
+ result_code = 0
+ controls = []
+
+ while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be ))
+ case pdu.app_tag
+ when 4 # search-data
+ yield( pdu.search_entry ) if block_given?
+ when 19 # search-referral
+ if return_referrals
+ if block_given?
+ se = Net::LDAP::Entry.new
+ se[:search_referrals] = (pdu.search_referrals || [])
+ yield se
+ end
+ end
+ #p pdu.referrals
+ when 5 # search-result
+ result_code = pdu.result_code
+ controls = pdu.result_controls
+ break
+ else
+ raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" )
+ end
+ end
+
+ # When we get here, we have seen a type-5 response.
+ # If there is no error AND there is an RFC-2696 cookie,
+ # then query again for the next page of results.
+ # If not, we're done.
+ # Don't screw this up or we'll break every search we do.
+ more_pages = false
+ if result_code == 0 and controls
+ controls.each do |c|
+ if c.oid == LdapControls::PagedResults
+ more_pages = false # just in case some bogus server sends us >1 of these.
+ if c.value and c.value.length > 0
+ cookie = c.value.read_ber[1]
+ if cookie and cookie.length > 0
+ rfc2696_cookie[1] = cookie
+ more_pages = true
+ end
+ end
+ end
+ end
+ end
+
+ break unless more_pages
+ } # loop
+
+ result_code
+ end
+
+
+
+
+ #--
+ # modify
+ # TODO, need to support a time limit, in case the server fails to respond.
+ # TODO!!! We're throwing an exception here on empty DN.
+ # Should return a proper error instead, probaby from farther up the chain.
+ # TODO!!! If the user specifies a bogus opcode, we'll throw a
+ # confusing error here ("to_ber_enumerated is not defined on nil").
+ #
+ def modify args
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
+ modify_ops = []
+ a = args[:operations] and a.each {|op, attr, values|
+ # TODO, fix the following line, which gives a bogus error
+ # if the opcode is invalid.
+ op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated
+ modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence
+ }
+
+ request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6)
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
+ @conn.write pkt
+
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" )
+ pdu.result_code
+ end
+
+
+ #--
+ # add
+ # TODO, need to support a time limit, in case the server fails to respond.
+ #
+ def add args
+ add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN")
+ add_attrs = []
+ a = args[:attributes] and a.each {|k,v|
+ add_attrs << [ k.to_s.to_ber, v.to_a.map {|m| m.to_ber}.to_ber_set ].to_ber_sequence
+ }
+
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
+ @conn.write pkt
+
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" )
+ pdu.result_code
+ end
+
+
+ #--
+ # rename
+ # TODO, need to support a time limit, in case the server fails to respond.
+ #
+ def rename args
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
+ delete_attrs = args[:delete_attributes] ? true : false
+
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12)
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
+ @conn.write pkt
+
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
+ pdu.result_code
+ end
+
+
+ #--
+ # delete
+ # TODO, need to support a time limit, in case the server fails to respond.
+ #
+ def delete args
+ dn = args[:dn] or raise "Unable to delete empty DN"
+
+ request = dn.to_s.to_ber_application_string(10)
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
+ @conn.write pkt
+
+ (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 11) or raise LdapError.new( "response missing or invalid" )
+ pdu.result_code
+ end
+
+
+ end # class Connection
+ end # class LDAP
+
+
+end # module Net
+
+
--- /dev/null
+# $Id: dataset.rb 78 2006-04-26 02:57:34Z blackhedd $
+#
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+#
+
+
+
+
+module Net
+class LDAP
+
+class Dataset < Hash
+
+ attr_reader :comments
+
+
+ def Dataset::read_ldif io
+ ds = Dataset.new
+
+ line = io.gets && chomp
+ dn = nil
+
+ while line
+ io.gets and chomp
+ if $_ =~ /^[\s]+/
+ line << " " << $'
+ else
+ nextline = $_
+
+ if line =~ /^\#/
+ ds.comments << line
+ elsif line =~ /^dn:[\s]*/i
+ dn = $'
+ ds[dn] = Hash.new {|k,v| k[v] = []}
+ elsif line.length == 0
+ dn = nil
+ elsif line =~ /^([^:]+):([\:]?)[\s]*/
+ # $1 is the attribute name
+ # $2 is a colon iff the attr-value is base-64 encoded
+ # $' is the attr-value
+ # Avoid the Base64 class because not all Ruby versions have it.
+ attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
+ ds[dn][$1.downcase.intern] << attrvalue
+ end
+
+ line = nextline
+ end
+ end
+
+ ds
+ end
+
+
+ def initialize
+ @comments = []
+ end
+
+
+ def to_ldif
+ ary = []
+ ary += (@comments || [])
+
+ keys.sort.each {|dn|
+ ary << "dn: #{dn}"
+
+ self[dn].keys.map {|sym| sym.to_s}.sort.each {|attr|
+ self[dn][attr.intern].each {|val|
+ ary << "#{attr}: #{val}"
+ }
+ }
+
+ ary << ""
+ }
+
+ block_given? and ary.each {|line| yield line}
+
+ ary
+ end
+
+
+end # Dataset
+
+end # LDAP
+end # Net
+
+
--- /dev/null
+# $Id: entry.rb 123 2006-05-18 03:52:38Z blackhedd $
+#
+# LDAP Entry (search-result) support classes
+#
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+
+
+
+
+module Net
+class LDAP
+
+
+ # Objects of this class represent individual entries in an LDAP
+ # directory. User code generally does not instantiate this class.
+ # Net::LDAP#search provides objects of this class to user code,
+ # either as block parameters or as return values.
+ #
+ # In LDAP-land, an "entry" is a collection of attributes that are
+ # uniquely and globally identified by a DN ("Distinguished Name").
+ # Attributes are identified by short, descriptive words or phrases.
+ # Although a directory is
+ # free to implement any attribute name, most of them follow rigorous
+ # standards so that the range of commonly-encountered attribute
+ # names is not large.
+ #
+ # An attribute name is case-insensitive. Most directories also
+ # restrict the range of characters allowed in attribute names.
+ # To simplify handling attribute names, Net::LDAP::Entry
+ # internally converts them to a standard format. Therefore, the
+ # methods which take attribute names can take Strings or Symbols,
+ # and work correctly regardless of case or capitalization.
+ #
+ # An attribute consists of zero or more data items called
+ # <i>values.</i> An entry is the combination of a unique DN, a set of attribute
+ # names, and a (possibly-empty) array of values for each attribute.
+ #
+ # Class Net::LDAP::Entry provides convenience methods for dealing
+ # with LDAP entries.
+ # In addition to the methods documented below, you may access individual
+ # attributes of an entry simply by giving the attribute name as
+ # the name of a method call. For example:
+ # ldap.search( ... ) do |entry|
+ # puts "Common name: #{entry.cn}"
+ # puts "Email addresses:"
+ # entry.mail.each {|ma| puts ma}
+ # end
+ # If you use this technique to access an attribute that is not present
+ # in a particular Entry object, a NoMethodError exception will be raised.
+ #
+ #--
+ # Ugly problem to fix someday: We key off the internal hash with
+ # a canonical form of the attribute name: convert to a string,
+ # downcase, then take the symbol. Unfortunately we do this in
+ # at least three places. Should do it in ONE place.
+ class Entry
+
+ # This constructor is not generally called by user code.
+ def initialize dn = nil # :nodoc:
+ @myhash = Hash.new {|k,v| k[v] = [] }
+ @myhash[:dn] = [dn]
+ end
+
+
+ def []= name, value # :nodoc:
+ sym = name.to_s.downcase.intern
+ @myhash[sym] = value
+ end
+
+
+ #--
+ # We have to deal with this one as we do with []=
+ # because this one and not the other one gets called
+ # in formulations like entry["CN"] << cn.
+ #
+ def [] name # :nodoc:
+ name = name.to_s.downcase.intern unless name.is_a?(Symbol)
+ @myhash[name]
+ end
+
+ # Returns the dn of the Entry as a String.
+ def dn
+ self[:dn][0]
+ end
+
+ # Returns an array of the attribute names present in the Entry.
+ def attribute_names
+ @myhash.keys
+ end
+
+ # Accesses each of the attributes present in the Entry.
+ # Calls a user-supplied block with each attribute in turn,
+ # passing two arguments to the block: a Symbol giving
+ # the name of the attribute, and a (possibly empty)
+ # Array of data values.
+ #
+ def each
+ if block_given?
+ attribute_names.each {|a|
+ attr_name,values = a,self[a]
+ yield attr_name, values
+ }
+ end
+ end
+
+ alias_method :each_attribute, :each
+
+
+ #--
+ # Convenience method to convert unknown method names
+ # to attribute references. Of course the method name
+ # comes to us as a symbol, so let's save a little time
+ # and not bother with the to_s.downcase two-step.
+ # Of course that means that a method name like mAIL
+ # won't work, but we shouldn't be encouraging that
+ # kind of bad behavior in the first place.
+ # Maybe we should thow something if the caller sends
+ # arguments or a block...
+ #
+ def method_missing *args, &block # :nodoc:
+ s = args[0].to_s.downcase.intern
+ if attribute_names.include?(s)
+ self[s]
+ elsif s.to_s[-1] == 61 and s.to_s.length > 1
+ value = args[1] or raise RuntimeError.new( "unable to set value" )
+ value = [value] unless value.is_a?(Array)
+ name = s.to_s[0..-2].intern
+ self[name] = value
+ else
+ raise NoMethodError.new( "undefined method '#{s}'" )
+ end
+ end
+
+ def write
+ end
+
+ end # class Entry
+
+
+end # class LDAP
+end # module Net
+
+
--- /dev/null
+# $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
+#
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+#
+
+
+module Net
+class LDAP
+
+
+# Class Net::LDAP::Filter is used to constrain
+# LDAP searches. An object of this class is
+# passed to Net::LDAP#search in the parameter :filter.
+#
+# Net::LDAP::Filter supports the complete set of search filters
+# available in LDAP, including conjunction, disjunction and negation
+# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
+# standard notation for specifying LDAP search filters.
+#
+# Here's how to code the familiar "objectclass is present" filter:
+# f = Net::LDAP::Filter.pres( "objectclass" )
+# The object returned by this code can be passed directly to
+# the <tt>:filter</tt> parameter of Net::LDAP#search.
+#
+# See the individual class and instance methods below for more examples.
+#
+class Filter
+
+ def initialize op, a, b
+ @op = op
+ @left = a
+ @right = b
+ end
+
+ # #eq creates a filter object indicating that the value of
+ # a paticular attribute must be either <i>present</i> or must
+ # match a particular string.
+ #
+ # To specify that an attribute is "present" means that only
+ # directory entries which contain a value for the particular
+ # attribute will be selected by the filter. This is useful
+ # in case of optional attributes such as <tt>mail.</tt>
+ # Presence is indicated by giving the value "*" in the second
+ # parameter to #eq. This example selects only entries that have
+ # one or more values for <tt>sAMAccountName:</tt>
+ # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
+ #
+ # To match a particular range of values, pass a string as the
+ # second parameter to #eq. The string may contain one or more
+ # "*" characters as wildcards: these match zero or more occurrences
+ # of any character. Full regular-expressions are <i>not</i> supported
+ # due to limitations in the underlying LDAP protocol.
+ # This example selects any entry with a <tt>mail</tt> value containing
+ # the substring "anderson":
+ # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
+ #--
+ # Removed gt and lt. They ain't in the standard!
+ #
+ def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
+ def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
+ #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
+ #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
+ def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
+ def Filter::le attribute, value; Filter.new :le, attribute, value; end
+
+ # #pres( attribute ) is a synonym for #eq( attribute, "*" )
+ #
+ def Filter::pres attribute; Filter.eq attribute, "*"; end
+
+ # operator & ("AND") is used to conjoin two or more filters.
+ # This expression will select only entries that have an <tt>objectclass</tt>
+ # attribute AND have a <tt>mail</tt> attribute that begins with "George":
+ # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
+ #
+ def & filter; Filter.new :and, self, filter; end
+
+ # operator | ("OR") is used to disjoin two or more filters.
+ # This expression will select entries that have either an <tt>objectclass</tt>
+ # attribute OR a <tt>mail</tt> attribute that begins with "George":
+ # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
+ #
+ def | filter; Filter.new :or, self, filter; end
+
+
+ #
+ # operator ~ ("NOT") is used to negate a filter.
+ # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
+ # attribute:
+ # f = ~ Net::LDAP::Filter.pres( "objectclass" )
+ #
+ #--
+ # This operator can't be !, evidently. Try it.
+ # Removed GT and LT. They're not in the RFC.
+ def ~@; Filter.new :not, self, nil; end
+
+
+ def to_s
+ case @op
+ when :ne
+ "(!(#{@left}=#{@right}))"
+ when :eq
+ "(#{@left}=#{@right})"
+ #when :gt
+ # "#{@left}>#{@right}"
+ #when :lt
+ # "#{@left}<#{@right}"
+ when :ge
+ "#{@left}>=#{@right}"
+ when :le
+ "#{@left}<=#{@right}"
+ when :and
+ "(&(#{@left})(#{@right}))"
+ when :or
+ "(|(#{@left})(#{@right}))"
+ when :not
+ "(!(#{@left}))"
+ else
+ raise "invalid or unsupported operator in LDAP Filter"
+ end
+ end
+
+
+ #--
+ # to_ber
+ # Filter ::=
+ # CHOICE {
+ # and [0] SET OF Filter,
+ # or [1] SET OF Filter,
+ # not [2] Filter,
+ # equalityMatch [3] AttributeValueAssertion,
+ # substrings [4] SubstringFilter,
+ # greaterOrEqual [5] AttributeValueAssertion,
+ # lessOrEqual [6] AttributeValueAssertion,
+ # present [7] AttributeType,
+ # approxMatch [8] AttributeValueAssertion
+ # }
+ #
+ # SubstringFilter
+ # SEQUENCE {
+ # type AttributeType,
+ # SEQUENCE OF CHOICE {
+ # initial [0] LDAPString,
+ # any [1] LDAPString,
+ # final [2] LDAPString
+ # }
+ # }
+ #
+ # Parsing substrings is a little tricky.
+ # We use the split method to break a string into substrings
+ # delimited by the * (star) character. But we also need
+ # to know whether there is a star at the head and tail
+ # of the string. A Ruby particularity comes into play here:
+ # if you split on * and the first character of the string is
+ # a star, then split will return an array whose first element
+ # is an _empty_ string. But if the _last_ character of the
+ # string is star, then split will return an array that does
+ # _not_ add an empty string at the end. So we have to deal
+ # with all that specifically.
+ #
+ def to_ber
+ case @op
+ when :eq
+ if @right == "*" # present
+ @left.to_s.to_ber_contextspecific 7
+ elsif @right =~ /[\*]/ #substring
+ ary = @right.split( /[\*]+/ )
+ final_star = @right =~ /[\*]$/
+ initial_star = ary.first == "" and ary.shift
+
+ seq = []
+ unless initial_star
+ seq << ary.shift.to_ber_contextspecific(0)
+ end
+ n_any_strings = ary.length - (final_star ? 0 : 1)
+ #p n_any_strings
+ n_any_strings.times {
+ seq << ary.shift.to_ber_contextspecific(1)
+ }
+ unless final_star
+ seq << ary.shift.to_ber_contextspecific(2)
+ end
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
+ else #equality
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
+ end
+ when :ge
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
+ when :le
+ [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
+ when :and
+ ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
+ ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
+ when :or
+ ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
+ ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
+ when :not
+ [@left.to_ber].to_ber_contextspecific 2
+ else
+ # ERROR, we'll return objectclass=* to keep things from blowing up,
+ # but that ain't a good answer and we need to kick out an error of some kind.
+ raise "unimplemented search filter"
+ end
+ end
+
+ #--
+ # coalesce
+ # This is a private helper method for dealing with chains of ANDs and ORs
+ # that are longer than two. If BOTH of our branches are of the specified
+ # type of joining operator, then return both of them as an array (calling
+ # coalesce recursively). If they're not, then return an array consisting
+ # only of self.
+ #
+ def coalesce operator
+ if @op == operator
+ [@left.coalesce( operator ), @right.coalesce( operator )]
+ else
+ [self]
+ end
+ end
+
+
+
+ #--
+ # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
+ # object. Convert it to a Net::LDAP::Filter.
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
+ # filter types. Could pull them out into a constant.
+ #
+ def Filter::parse_ldap_filter obj
+ case obj.ber_identifier
+ when 0x87 # present. context-specific primitive 7.
+ Filter.eq( obj.to_s, "*" )
+ when 0xa3 # equalityMatch. context-specific constructed 3.
+ Filter.eq( obj[0], obj[1] )
+ else
+ raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
+ end
+ end
+
+
+ #--
+ # We got a hash of attribute values.
+ # Do we match the attributes?
+ # Return T/F, and call match recursively as necessary.
+ def match entry
+ case @op
+ when :eq
+ if @right == "*"
+ l = entry[@left] and l.length > 0
+ else
+ l = entry[@left] and l = l.to_a and l.index(@right)
+ end
+ else
+ raise LdapError.new( "unknown filter type in match: #{@op}" )
+ end
+ end
+
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
+ # to a Net::LDAP::Filter.
+ def self.construct ldap_filter_string
+ FilterParser.new(ldap_filter_string).filter
+ end
+
+ # Synonym for #construct.
+ # to a Net::LDAP::Filter.
+ def self.from_rfc2254 ldap_filter_string
+ construct ldap_filter_string
+ end
+
+end # class Net::LDAP::Filter
+
+
+
+class FilterParser #:nodoc:
+
+ attr_reader :filter
+
+ def initialize str
+ require 'strscan'
+ @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
+ end
+
+ def parse scanner
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
+ end
+
+ def parse_paren_expression scanner
+ if scanner.scan(/\s*\(\s*/)
+ b = if scanner.scan(/\s*\&\s*/)
+ a = nil
+ branches = []
+ while br = parse_paren_expression(scanner)
+ branches << br
+ end
+ if branches.length >= 2
+ a = branches.shift
+ while branches.length > 0
+ a = a & branches.shift
+ end
+ a
+ end
+ elsif scanner.scan(/\s*\|\s*/)
+ # TODO: DRY!
+ a = nil
+ branches = []
+ while br = parse_paren_expression(scanner)
+ branches << br
+ end
+ if branches.length >= 2
+ a = branches.shift
+ while branches.length > 0
+ a = a | branches.shift
+ end
+ a
+ end
+ elsif scanner.scan(/\s*\!\s*/)
+ br = parse_paren_expression(scanner)
+ if br
+ ~ br
+ end
+ else
+ parse_filter_branch( scanner )
+ end
+
+ if b and scanner.scan( /\s*\)\s*/ )
+ b
+ end
+ end
+ end
+
+ # Added a greatly-augmented filter contributed by Andre Nathan
+ # for detecting special characters in values. (15Aug06)
+ def parse_filter_branch scanner
+ scanner.scan(/\s*/)
+ if token = scanner.scan( /[\w\-_]+/ )
+ scanner.scan(/\s*/)
+ if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
+ scanner.scan(/\s*/)
+ #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
+ if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
+ case op
+ when "="
+ Filter.eq( token, value )
+ when "!="
+ Filter.ne( token, value )
+ when "<"
+ Filter.lt( token, value )
+ when "<="
+ Filter.le( token, value )
+ when ">"
+ Filter.gt( token, value )
+ when ">="
+ Filter.ge( token, value )
+ end
+ end
+ end
+ end
+ end
+
+end # class Net::LDAP::FilterParser
+
+end # class Net::LDAP
+end # module Net
+
+
--- /dev/null
+# $Id: pdu.rb 126 2006-05-31 15:55:16Z blackhedd $
+#
+# LDAP PDU support classes
+#
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+
+
+
+module Net
+
+
+class LdapPduError < Exception; end
+
+
+class LdapPdu
+
+ BindResult = 1
+ SearchReturnedData = 4
+ SearchResult = 5
+ ModifyResponse = 7
+ AddResponse = 9
+ DeleteResponse = 11
+ ModifyRDNResponse = 13
+ SearchResultReferral = 19
+
+ attr_reader :msg_id, :app_tag
+ attr_reader :search_dn, :search_attributes, :search_entry
+ attr_reader :search_referrals
+
+ #
+ # initialize
+ # An LDAP PDU always looks like a BerSequence with
+ # at least two elements: an integer (message-id number), and
+ # an application-specific sequence.
+ # Some LDAPv3 packets also include an optional
+ # third element, which is a sequence of "controls"
+ # (See RFC 2251, section 4.1.12).
+ # The application-specific tag in the sequence tells
+ # us what kind of packet it is, and each kind has its
+ # own format, defined in RFC-1777.
+ # Observe that many clients (such as ldapsearch)
+ # do not necessarily enforce the expected application
+ # tags on received protocol packets. This implementation
+ # does interpret the RFC strictly in this regard, and
+ # it remains to be seen whether there are servers out
+ # there that will not work well with our approach.
+ #
+ # Added a controls-processor to SearchResult.
+ # Didn't add it everywhere because it just _feels_
+ # like it will need to be refactored.
+ #
+ def initialize ber_object
+ begin
+ @msg_id = ber_object[0].to_i
+ @app_tag = ber_object[1].ber_identifier - 0x60
+ rescue
+ # any error becomes a data-format error
+ raise LdapPduError.new( "ldap-pdu format error" )
+ end
+
+ case @app_tag
+ when BindResult
+ parse_ldap_result ber_object[1]
+ when SearchReturnedData
+ parse_search_return ber_object[1]
+ when SearchResultReferral
+ parse_search_referral ber_object[1]
+ when SearchResult
+ parse_ldap_result ber_object[1]
+ parse_controls(ber_object[2]) if ber_object[2]
+ when ModifyResponse
+ parse_ldap_result ber_object[1]
+ when AddResponse
+ parse_ldap_result ber_object[1]
+ when DeleteResponse
+ parse_ldap_result ber_object[1]
+ when ModifyRDNResponse
+ parse_ldap_result ber_object[1]
+ else
+ raise LdapPduError.new( "unknown pdu-type: #{@app_tag}" )
+ end
+ end
+
+ #
+ # result_code
+ # This returns an LDAP result code taken from the PDU,
+ # but it will be nil if there wasn't a result code.
+ # That can easily happen depending on the type of packet.
+ #
+ def result_code code = :resultCode
+ @ldap_result and @ldap_result[code]
+ end
+
+ # Return RFC-2251 Controls if any.
+ # Messy. Does this functionality belong somewhere else?
+ def result_controls
+ @ldap_controls || []
+ end
+
+
+ #
+ # parse_ldap_result
+ #
+ def parse_ldap_result sequence
+ sequence.length >= 3 or raise LdapPduError
+ @ldap_result = {:resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2]}
+ end
+ private :parse_ldap_result
+
+ #
+ # parse_search_return
+ # Definition from RFC 1777 (we're handling application-4 here)
+ #
+ # Search Response ::=
+ # CHOICE {
+ # entry [APPLICATION 4] SEQUENCE {
+ # objectName LDAPDN,
+ # attributes SEQUENCE OF SEQUENCE {
+ # AttributeType,
+ # SET OF AttributeValue
+ # }
+ # },
+ # resultCode [APPLICATION 5] LDAPResult
+ # }
+ #
+ # We concoct a search response that is a hash of the returned attribute values.
+ # NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES.
+ # This is to make them more predictable for user programs, but it
+ # may not be a good idea. Maybe this should be configurable.
+ # ALTERNATE IMPLEMENTATION: In addition to @search_dn and @search_attributes,
+ # we also return @search_entry, which is an LDAP::Entry object.
+ # If that works out well, then we'll remove the first two.
+ #
+ # Provisionally removed obsolete search_attributes and search_dn, 04May06.
+ #
+ def parse_search_return sequence
+ sequence.length >= 2 or raise LdapPduError
+ @search_entry = LDAP::Entry.new( sequence[0] )
+ #@search_dn = sequence[0]
+ #@search_attributes = {}
+ sequence[1].each {|seq|
+ @search_entry[seq[0]] = seq[1]
+ #@search_attributes[seq[0].downcase.intern] = seq[1]
+ }
+ end
+
+ #
+ # A search referral is a sequence of one or more LDAP URIs.
+ # Any number of search-referral replies can be returned by the server, interspersed
+ # with normal replies in any order.
+ # Until I can think of a better way to do this, we'll return the referrals as an array.
+ # It'll be up to higher-level handlers to expose something reasonable to the client.
+ def parse_search_referral uris
+ @search_referrals = uris
+ end
+
+
+ # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting
+ # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL
+ # Octet String. If only two fields are given, the second one may be
+ # either criticality or data, since criticality has a default value.
+ # Someday we may want to come back here and add support for some of
+ # more-widely used controls. RFC-2696 is a good example.
+ #
+ def parse_controls sequence
+ @ldap_controls = sequence.map do |control|
+ o = OpenStruct.new
+ o.oid,o.criticality,o.value = control[0],control[1],control[2]
+ if o.criticality and o.criticality.is_a?(String)
+ o.value = o.criticality
+ o.criticality = false
+ end
+ o
+ end
+ end
+ private :parse_controls
+
+
+end
+
+
+end # module Net
+
--- /dev/null
+# $Id: psw.rb 73 2006-04-24 21:59:35Z blackhedd $
+#
+#
+#----------------------------------------------------------------------------
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#---------------------------------------------------------------------------
+#
+#
+
+
+module Net
+class LDAP
+
+
+class Password
+ class << self
+
+ # Generate a password-hash suitable for inclusion in an LDAP attribute.
+ # Pass a hash type (currently supported: :md5 and :sha) and a plaintext
+ # password. This function will return a hashed representation.
+ # STUB: This is here to fulfill the requirements of an RFC, which one?
+ # TODO, gotta do salted-sha and (maybe) salted-md5.
+ # Should we provide sha1 as a synonym for sha1? I vote no because then
+ # should you also provide ssha1 for symmetry?
+ def generate( type, str )
+ case type
+ when :md5
+ require 'md5'
+ "{MD5}#{ [MD5.new( str.to_s ).digest].pack("m").chomp }"
+ when :sha
+ require 'sha1'
+ "{SHA}#{ [SHA1.new( str.to_s ).digest].pack("m").chomp }"
+ # when ssha
+ else
+ raise Net::LDAP::LdapError.new( "unsupported password-hash type (#{type})" )
+ end
+ end
+
+ end
+end
+
+
+end # class LDAP
+end # module Net
+
+
--- /dev/null
+# $Id: ldif.rb 78 2006-04-26 02:57:34Z blackhedd $
+#
+# Net::LDIF for Ruby
+#
+#
+#
+# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
+#
+# Gmail: garbagecat10
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+#
+
+# THIS FILE IS A STUB.
+
+module Net
+
+ class LDIF
+
+
+ end # class LDIF
+
+
+end # module Net
+
+
--- /dev/null
+# $Id: testber.rb 57 2006-04-18 00:18:48Z blackhedd $
+#
+#
+
+
+$:.unshift "lib"
+
+require 'net/ldap'
+require 'stringio'
+
+
+class TestBer < Test::Unit::TestCase
+
+ def setup
+ end
+
+ # TODO: Add some much bigger numbers
+ # 5000000000 is a Bignum, which hits different code.
+ def test_ber_integers
+ assert_equal( "\002\001\005", 5.to_ber )
+ assert_equal( "\002\002\203t", 500.to_ber )
+ assert_equal( "\002\003\203\206P", 50000.to_ber )
+ assert_equal( "\002\005\222\320\227\344\000", 5000000000.to_ber )
+ end
+
+ def test_ber_parsing
+ assert_equal( 6, "\002\001\006".read_ber( Net::LDAP::AsnSyntax ))
+ assert_equal( "testing", "\004\007testing".read_ber( Net::LDAP::AsnSyntax ))
+ end
+
+
+ def test_ber_parser_on_ldap_bind_request
+ s = StringIO.new "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus"
+ assert_equal( [1, [3, "Administrator", "ad_is_bogus"]], s.read_ber( Net::LDAP::AsnSyntax ))
+ end
+
+
+
+
+end
+
+
--- /dev/null
+# $Id: testdata.ldif 50 2006-04-17 17:57:33Z blackhedd $
+#
+# This is test-data for an LDAP server in LDIF format.
+#
+dn: dc=bayshorenetworks,dc=com
+objectClass: dcObject
+objectClass: organization
+o: Bayshore Networks LLC
+dc: bayshorenetworks
+
+dn: cn=Manager,dc=bayshorenetworks,dc=com
+objectClass: organizationalrole
+cn: Manager
+
+dn: ou=people,dc=bayshorenetworks,dc=com
+objectClass: organizationalunit
+ou: people
+
+dn: ou=privileges,dc=bayshorenetworks,dc=com
+objectClass: organizationalunit
+ou: privileges
+
+dn: ou=roles,dc=bayshorenetworks,dc=com
+objectClass: organizationalunit
+ou: roles
+
+dn: ou=office,dc=bayshorenetworks,dc=com
+objectClass: organizationalunit
+ou: office
+
+dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
+cn: Bob Fosse
+mail: nogoodnik@steamheat.net
+sn: Fosse
+ou: people
+objectClass: top
+objectClass: inetorgperson
+objectClass: authorizedperson
+hasAccessRole: uniqueIdentifier=engineer,ou=roles
+hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles
+hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
+hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
+hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles
+hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles
+hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles
+hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles
+hasAccessRole: uniqueIdentifier=workorder_user,ou=roles
+hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles
+hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles
+hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles
+
+dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
+cn: Gwen Verdon
+mail: elephant@steamheat.net
+sn: Verdon
+ou: people
+objectClass: top
+objectClass: inetorgperson
+objectClass: authorizedperson
+hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles
+hasAccessRole: uniqueIdentifier=engineer,ou=roles
+hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
+hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
+hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles
+
+dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com
+uniqueIdentifier: engineering
+ou: privileges
+objectClass: accessPrivilege
+
+dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com
+uniqueIdentifier: engineer
+ou: roles
+objectClass: accessRole
+hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges
+
+dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com
+uniqueIdentifier: ldapadmin
+ou: roles
+objectClass: accessRole
+
+dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com
+uniqueIdentifier: ldapsuperadmin
+ou: roles
+objectClass: accessRole
+
+dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com
+cn: Sid Sorokin
+mail: catperson@steamheat.net
+sn: Sorokin
+ou: people
+objectClass: top
+objectClass: inetorgperson
+objectClass: authorizedperson
+hasAccessRole: uniqueIdentifier=engineer,ou=roles
+hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles
+hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles
+hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles
+hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles
+hasAccessRole: uniqueIdentifier=workorder_user,ou=roles
+
--- /dev/null
+# $Id: testem.rb 121 2006-05-15 18:36:24Z blackhedd $
+#
+#
+
+require 'test/unit'
+require 'tests/testber'
+require 'tests/testldif'
+require 'tests/testldap'
+require 'tests/testpsw'
+require 'tests/testfilter'
+
+
--- /dev/null
+# $Id: testfilter.rb 122 2006-05-15 20:03:56Z blackhedd $
+#
+#
+
+require 'test/unit'
+
+$:.unshift "lib"
+
+require 'net/ldap'
+
+
+class TestFilter < Test::Unit::TestCase
+
+ def setup
+ end
+
+
+ def teardown
+ end
+
+ def test_rfc_2254
+ p Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " )
+ p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
+ p Net::LDAP::Filter.from_rfc2254( "uid<george*" )
+ p Net::LDAP::Filter.from_rfc2254( "uid <= george*" )
+ p Net::LDAP::Filter.from_rfc2254( "uid>george*" )
+ p Net::LDAP::Filter.from_rfc2254( "uid>=george*" )
+ p Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
+
+ p Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" )
+ p Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" )
+ p Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" )
+ end
+
+
+end
+
--- /dev/null
+# $Id: testldap.rb 65 2006-04-23 01:17:49Z blackhedd $
+#
+#
+
+
+$:.unshift "lib"
+
+require 'test/unit'
+
+require 'net/ldap'
+require 'stringio'
+
+
+class TestLdapClient < Test::Unit::TestCase
+
+ # TODO: these tests crash and burn if the associated
+ # LDAP testserver isn't up and running.
+ # We rely on being able to read a file with test data
+ # in LDIF format.
+ # TODO, WARNING: for the moment, this data is in a file
+ # whose name and location are HARDCODED into the
+ # instance method load_test_data.
+
+ def setup
+ @host = "127.0.0.1"
+ @port = 3890
+ @auth = {
+ :method => :simple,
+ :username => "cn=bigshot,dc=bayshorenetworks,dc=com",
+ :password => "opensesame"
+ }
+
+ @ldif = load_test_data
+ end
+
+
+
+ # Get some test data which will be used to validate
+ # the responses from the test LDAP server we will
+ # connect to.
+ # TODO, Bogus: we are HARDCODING the location of the file for now.
+ #
+ def load_test_data
+ ary = File.readlines( "tests/testdata.ldif" )
+ hash = {}
+ while line = ary.shift and line.chomp!
+ if line =~ /^dn:[\s]*/i
+ dn = $'
+ hash[dn] = {}
+ while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/
+ hash[dn][$1.downcase.intern] ||= []
+ hash[dn][$1.downcase.intern] << $'
+ end
+ end
+ end
+ hash
+ end
+
+
+
+ # Binding tests.
+ # Need tests for all kinds of network failures and incorrect auth.
+ # TODO: Implement a class-level timeout for operations like bind.
+ # Search has a timeout defined at the protocol level, other ops do not.
+ # TODO, use constants for the LDAP result codes, rather than hardcoding them.
+ def test_bind
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
+ assert_equal( true, ldap.bind )
+ assert_equal( 0, ldap.get_operation_result.code )
+ assert_equal( "Success", ldap.get_operation_result.message )
+
+ bad_username = @auth.merge( {:username => "cn=badguy,dc=imposters,dc=com"} )
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_username
+ assert_equal( false, ldap.bind )
+ assert_equal( 48, ldap.get_operation_result.code )
+ assert_equal( "Inappropriate Authentication", ldap.get_operation_result.message )
+
+ bad_password = @auth.merge( {:password => "cornhusk"} )
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => bad_password
+ assert_equal( false, ldap.bind )
+ assert_equal( 49, ldap.get_operation_result.code )
+ assert_equal( "Invalid Credentials", ldap.get_operation_result.message )
+ end
+
+
+
+ def test_search
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
+
+ search = {:base => "dc=smalldomain,dc=com"}
+ assert_equal( false, ldap.search( search ))
+ assert_equal( 32, ldap.get_operation_result.code )
+
+ search = {:base => "dc=bayshorenetworks,dc=com"}
+ assert_equal( true, ldap.search( search ))
+ assert_equal( 0, ldap.get_operation_result.code )
+
+ ldap.search( search ) {|res|
+ assert_equal( res, @ldif )
+ }
+ end
+
+
+
+
+ # This is a helper routine for test_search_attributes.
+ def internal_test_search_attributes attrs_to_search
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
+ assert( ldap.bind )
+
+ search = {
+ :base => "dc=bayshorenetworks,dc=com",
+ :attributes => attrs_to_search
+ }
+
+ ldif = @ldif
+ ldif.each {|dn,entry|
+ entry.delete_if {|attr,value|
+ ! attrs_to_search.include?(attr)
+ }
+ }
+
+ assert_equal( true, ldap.search( search ))
+ ldap.search( search ) {|res|
+ res_keys = res.keys.sort
+ ldif_keys = ldif.keys.sort
+ assert( res_keys, ldif_keys )
+ res.keys.each {|rk|
+ assert( res[rk], ldif[rk] )
+ }
+ }
+ end
+
+
+ def test_search_attributes
+ internal_test_search_attributes [:mail]
+ internal_test_search_attributes [:cn]
+ internal_test_search_attributes [:ou]
+ internal_test_search_attributes [:hasaccessprivilege]
+ internal_test_search_attributes ["mail"]
+ internal_test_search_attributes ["cn"]
+ internal_test_search_attributes ["ou"]
+ internal_test_search_attributes ["hasaccessrole"]
+
+ internal_test_search_attributes [:mail, :cn, :ou, :hasaccessrole]
+ internal_test_search_attributes [:mail, "cn", :ou, "hasaccessrole"]
+ end
+
+
+ def test_search_filters
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
+ search = {
+ :base => "dc=bayshorenetworks,dc=com",
+ :filter => Net::LDAP::Filter.eq( "sn", "Fosse" )
+ }
+
+ ldap.search( search ) {|res|
+ p res
+ }
+ end
+
+
+
+ def test_open
+ ldap = Net::LDAP.new :host => @host, :port => @port, :auth => @auth
+ ldap.open {|ldap|
+ 10.times {
+ rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" )
+ assert_equal( true, rc )
+ }
+ }
+ end
+
+
+ def test_ldap_open
+ Net::LDAP.open( :host => @host, :port => @port, :auth => @auth ) {|ldap|
+ 10.times {
+ rc = ldap.search( :base => "dc=bayshorenetworks,dc=com" )
+ assert_equal( true, rc )
+ }
+ }
+ end
+
+
+
+
+
+end
+
+
--- /dev/null
+# $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $
+#
+#
+
+
+$:.unshift "lib"
+
+require 'test/unit'
+
+require 'net/ldap'
+require 'net/ldif'
+
+require 'sha1'
+require 'base64'
+
+class TestLdif < Test::Unit::TestCase
+
+ TestLdifFilename = "tests/testdata.ldif"
+
+ def test_empty_ldif
+ ds = Net::LDAP::Dataset::read_ldif( StringIO.new )
+ assert_equal( true, ds.empty? )
+ end
+
+ def test_ldif_with_comments
+ str = ["# Hello from LDIF-land", "# This is an unterminated comment"]
+ io = StringIO.new( str[0] + "\r\n" + str[1] )
+ ds = Net::LDAP::Dataset::read_ldif( io )
+ assert_equal( str, ds.comments )
+ end
+
+ def test_ldif_with_password
+ psw = "goldbricks"
+ hashed_psw = "{SHA}" + Base64::encode64( SHA1.new(psw).digest ).chomp
+
+ ldif_encoded = Base64::encode64( hashed_psw ).chomp
+ ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n" ))
+ recovered_psw = ds["Goldbrick"][:userpassword].shift
+ assert_equal( hashed_psw, recovered_psw )
+ end
+
+ def test_ldif_with_continuation_lines
+ ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: abcdefg\r\n hijklmn\r\n\r\n" ))
+ assert_equal( true, ds.has_key?( "abcdefg hijklmn" ))
+ end
+
+ # TODO, INADEQUATE. We need some more tests
+ # to verify the content.
+ def test_ldif
+ File.open( TestLdifFilename, "r" ) {|f|
+ ds = Net::LDAP::Dataset::read_ldif( f )
+ assert_equal( 13, ds.length )
+ }
+ end
+
+ # TODO, need some tests.
+ # Must test folded lines and base64-encoded lines as well as normal ones.
+ def test_to_ldif
+ File.open( TestLdifFilename, "r" ) {|f|
+ ds = Net::LDAP::Dataset::read_ldif( f )
+ ds.to_ldif
+ assert_equal( true, false ) # REMOVE WHEN WE HAVE SOME TESTS HERE.
+ }
+ end
+
+
+end
+
+
--- /dev/null
+# $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $
+#
+#
+
+
+$:.unshift "lib"
+
+require 'net/ldap'
+require 'stringio'
+
+
+class TestPassword < Test::Unit::TestCase
+
+ def setup
+ end
+
+
+ def test_psw
+ assert_equal( "{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate( :md5, "cashflow" ))
+ assert_equal( "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate( :sha, "cashflow" ))
+ end
+
+
+
+
+end
+
+